From 299102fbc4c040fe91d5eb5435a78b5599ba64dd Mon Sep 17 00:00:00 2001 From: Yessiest Date: Fri, 26 Nov 2021 20:38:17 +0400 Subject: [PATCH] initial commit --- .gitignore | 1 + bot.lua | 33 + deps/base64.lua | 114 +++ deps/coro-channel.lua | 183 ++++ deps/coro-http.lua | 195 ++++ deps/coro-net.lua | 181 ++++ deps/coro-websocket.lua | 196 ++++ deps/coro-wrapper.lua | 151 +++ deps/discordia/docgen.lua | 373 +++++++ deps/discordia/examples/appender.lua | 28 + deps/discordia/examples/basicCommands.lua | 25 + deps/discordia/examples/embed.lua | 45 + .../discordia/examples/helpCommandExample.lua | 44 + deps/discordia/examples/pingPong.lua | 20 + deps/discordia/init.lua | 18 + deps/discordia/libs/class.lua | 165 ++++ deps/discordia/libs/client/API.lua | 711 ++++++++++++++ deps/discordia/libs/client/Client.lua | 679 +++++++++++++ deps/discordia/libs/client/EventHandler.lua | 540 ++++++++++ deps/discordia/libs/client/Resolver.lua | 202 ++++ deps/discordia/libs/client/Shard.lua | 252 +++++ deps/discordia/libs/client/WebSocket.lua | 121 +++ deps/discordia/libs/constants.lua | 17 + deps/discordia/libs/containers/Activity.lua | 148 +++ .../libs/containers/AuditLogEntry.lua | 227 +++++ deps/discordia/libs/containers/Ban.lua | 52 + deps/discordia/libs/containers/Emoji.lua | 168 ++++ .../libs/containers/GroupChannel.lua | 122 +++ deps/discordia/libs/containers/Guild.lua | 921 ++++++++++++++++++ .../libs/containers/GuildCategoryChannel.lua | 83 ++ .../libs/containers/GuildTextChannel.lua | 165 ++++ .../libs/containers/GuildVoiceChannel.lua | 138 +++ deps/discordia/libs/containers/Invite.lua | 187 ++++ deps/discordia/libs/containers/Member.lua | 539 ++++++++++ deps/discordia/libs/containers/Message.lua | 579 +++++++++++ .../libs/containers/PermissionOverwrite.lua | 234 +++++ .../libs/containers/PrivateChannel.lua | 37 + deps/discordia/libs/containers/Reaction.lua | 149 +++ .../libs/containers/Relationship.lua | 27 + deps/discordia/libs/containers/Role.lua | 383 ++++++++ deps/discordia/libs/containers/User.lua | 186 ++++ deps/discordia/libs/containers/Webhook.lua | 147 +++ .../libs/containers/abstract/Channel.lua | 66 ++ .../libs/containers/abstract/Container.lua | 68 ++ .../libs/containers/abstract/GuildChannel.lua | 281 ++++++ .../libs/containers/abstract/Snowflake.lua | 63 ++ .../libs/containers/abstract/TextChannel.lua | 326 +++++++ .../libs/containers/abstract/UserPresence.lua | 127 +++ deps/discordia/libs/endpoints.lua | 54 + deps/discordia/libs/enums.lua | 214 ++++ deps/discordia/libs/extensions.lua | 253 +++++ .../libs/iterables/ArrayIterable.lua | 108 ++ deps/discordia/libs/iterables/Cache.lua | 150 +++ .../libs/iterables/FilteredIterable.lua | 27 + deps/discordia/libs/iterables/Iterable.lua | 278 ++++++ .../libs/iterables/SecondaryCache.lua | 78 ++ .../libs/iterables/TableIterable.lua | 53 + deps/discordia/libs/iterables/WeakCache.lua | 25 + deps/discordia/libs/utils/Clock.lua | 56 ++ deps/discordia/libs/utils/Color.lua | 313 ++++++ deps/discordia/libs/utils/Date.lua | 394 ++++++++ deps/discordia/libs/utils/Deque.lua | 105 ++ deps/discordia/libs/utils/Emitter.lua | 226 +++++ deps/discordia/libs/utils/Logger.lua | 82 ++ deps/discordia/libs/utils/Mutex.lua | 68 ++ deps/discordia/libs/utils/Permissions.lua | 254 +++++ deps/discordia/libs/utils/Stopwatch.lua | 85 ++ deps/discordia/libs/utils/Time.lua | 277 ++++++ deps/discordia/libs/voice/VoiceConnection.lua | 432 ++++++++ deps/discordia/libs/voice/VoiceManager.lua | 33 + deps/discordia/libs/voice/VoiceSocket.lua | 197 ++++ deps/discordia/libs/voice/opus.lua | 241 +++++ deps/discordia/libs/voice/sodium.lua | 85 ++ .../libs/voice/streams/FFmpegProcess.lua | 88 ++ .../libs/voice/streams/PCMGenerator.lua | 18 + .../libs/voice/streams/PCMStream.lua | 28 + .../libs/voice/streams/PCMString.lua | 28 + deps/discordia/package.lua | 36 + deps/http-codec.lua | 301 ++++++ deps/pathjoin.lua | 124 +++ deps/resource.lua | 88 ++ deps/secure-socket/biowrap.lua | 115 +++ deps/secure-socket/context.lua | 121 +++ deps/secure-socket/init.lua | 34 + deps/secure-socket/package.lua | 12 + deps/secure-socket/root_ca.dat | Bin 0 -> 146914 bytes deps/sha1.lua | 194 ++++ deps/sqlite3.lua | 638 ++++++++++++ deps/websocket-codec.lua | 301 ++++++ libraries/.tasklib.lua.swp | Bin 0 -> 12288 bytes libraries/air.lua | 187 ++++ libraries/classes/.test/baseclass.lua | 1 + libraries/classes/.test/test.lua | 88 ++ libraries/classes/.test/tty-colors.lua | 1 + libraries/classes/acl.lua | 92 ++ libraries/classes/baseclass.lua | 32 + libraries/classes/command-handler.lua | 107 ++ libraries/classes/command.lua | 163 ++++ libraries/classes/emitter-proxy.lua | 95 ++ libraries/classes/interfaces/discord.lua | 12 + libraries/classes/monitor.lua | 90 ++ libraries/classes/plugin-handler.lua | 124 +++ libraries/classes/plugin.lua | 58 ++ libraries/classes/server-handler.lua | 91 ++ libraries/esolangs/befunge.lua | 436 +++++++++ libraries/esolangs/brainfuck.lua | 217 +++++ libraries/eventlist.lua | 30 + libraries/file.lua | 113 +++ libraries/import.lua | 47 + libraries/markov.lua | 122 +++ libraries/purify.lua | 32 + libraries/table-utils.lua | 85 ++ libraries/tasklib.lua | 38 + libraries/unixToString.lua | 9 + plugins/debug/init.lua | 5 + plugins/esolang/help.lua | 27 + plugins/esolang/init.lua | 115 +++ plugins/help/init.lua | 97 ++ plugins/meta/help.lua | 61 ++ plugins/meta/init.lua | 348 +++++++ plugins/plugins/init.lua | 110 +++ plugins/tools/init.lua | 206 ++++ resources/default.json | 1 + resources/freud.json | 1 + resources/markov_preset_generator/markov.lua | 122 +++ .../preset_generator.lua | 11 + resources/reddit.json | 1 + resources/travisscott.json | 1 + todo-stuff/enforcer/init.lua | 478 +++++++++ todo-stuff/reactions/init.lua | 340 +++++++ todo-stuff/settings/init.lua | 120 +++ todo-stuff/tasks/init.lua | 362 +++++++ 132 files changed, 20607 insertions(+) create mode 100644 .gitignore create mode 100644 bot.lua create mode 100644 deps/base64.lua create mode 100644 deps/coro-channel.lua create mode 100644 deps/coro-http.lua create mode 100644 deps/coro-net.lua create mode 100644 deps/coro-websocket.lua create mode 100644 deps/coro-wrapper.lua create mode 100644 deps/discordia/docgen.lua create mode 100644 deps/discordia/examples/appender.lua create mode 100644 deps/discordia/examples/basicCommands.lua create mode 100644 deps/discordia/examples/embed.lua create mode 100644 deps/discordia/examples/helpCommandExample.lua create mode 100644 deps/discordia/examples/pingPong.lua create mode 100644 deps/discordia/init.lua create mode 100644 deps/discordia/libs/class.lua create mode 100644 deps/discordia/libs/client/API.lua create mode 100644 deps/discordia/libs/client/Client.lua create mode 100644 deps/discordia/libs/client/EventHandler.lua create mode 100644 deps/discordia/libs/client/Resolver.lua create mode 100644 deps/discordia/libs/client/Shard.lua create mode 100644 deps/discordia/libs/client/WebSocket.lua create mode 100644 deps/discordia/libs/constants.lua create mode 100644 deps/discordia/libs/containers/Activity.lua create mode 100644 deps/discordia/libs/containers/AuditLogEntry.lua create mode 100644 deps/discordia/libs/containers/Ban.lua create mode 100644 deps/discordia/libs/containers/Emoji.lua create mode 100644 deps/discordia/libs/containers/GroupChannel.lua create mode 100644 deps/discordia/libs/containers/Guild.lua create mode 100644 deps/discordia/libs/containers/GuildCategoryChannel.lua create mode 100644 deps/discordia/libs/containers/GuildTextChannel.lua create mode 100644 deps/discordia/libs/containers/GuildVoiceChannel.lua create mode 100644 deps/discordia/libs/containers/Invite.lua create mode 100644 deps/discordia/libs/containers/Member.lua create mode 100644 deps/discordia/libs/containers/Message.lua create mode 100644 deps/discordia/libs/containers/PermissionOverwrite.lua create mode 100644 deps/discordia/libs/containers/PrivateChannel.lua create mode 100644 deps/discordia/libs/containers/Reaction.lua create mode 100644 deps/discordia/libs/containers/Relationship.lua create mode 100644 deps/discordia/libs/containers/Role.lua create mode 100644 deps/discordia/libs/containers/User.lua create mode 100644 deps/discordia/libs/containers/Webhook.lua create mode 100644 deps/discordia/libs/containers/abstract/Channel.lua create mode 100644 deps/discordia/libs/containers/abstract/Container.lua create mode 100644 deps/discordia/libs/containers/abstract/GuildChannel.lua create mode 100644 deps/discordia/libs/containers/abstract/Snowflake.lua create mode 100644 deps/discordia/libs/containers/abstract/TextChannel.lua create mode 100644 deps/discordia/libs/containers/abstract/UserPresence.lua create mode 100644 deps/discordia/libs/endpoints.lua create mode 100644 deps/discordia/libs/enums.lua create mode 100644 deps/discordia/libs/extensions.lua create mode 100644 deps/discordia/libs/iterables/ArrayIterable.lua create mode 100644 deps/discordia/libs/iterables/Cache.lua create mode 100644 deps/discordia/libs/iterables/FilteredIterable.lua create mode 100644 deps/discordia/libs/iterables/Iterable.lua create mode 100644 deps/discordia/libs/iterables/SecondaryCache.lua create mode 100644 deps/discordia/libs/iterables/TableIterable.lua create mode 100644 deps/discordia/libs/iterables/WeakCache.lua create mode 100644 deps/discordia/libs/utils/Clock.lua create mode 100644 deps/discordia/libs/utils/Color.lua create mode 100644 deps/discordia/libs/utils/Date.lua create mode 100644 deps/discordia/libs/utils/Deque.lua create mode 100644 deps/discordia/libs/utils/Emitter.lua create mode 100644 deps/discordia/libs/utils/Logger.lua create mode 100644 deps/discordia/libs/utils/Mutex.lua create mode 100644 deps/discordia/libs/utils/Permissions.lua create mode 100644 deps/discordia/libs/utils/Stopwatch.lua create mode 100644 deps/discordia/libs/utils/Time.lua create mode 100644 deps/discordia/libs/voice/VoiceConnection.lua create mode 100644 deps/discordia/libs/voice/VoiceManager.lua create mode 100644 deps/discordia/libs/voice/VoiceSocket.lua create mode 100644 deps/discordia/libs/voice/opus.lua create mode 100644 deps/discordia/libs/voice/sodium.lua create mode 100644 deps/discordia/libs/voice/streams/FFmpegProcess.lua create mode 100644 deps/discordia/libs/voice/streams/PCMGenerator.lua create mode 100644 deps/discordia/libs/voice/streams/PCMStream.lua create mode 100644 deps/discordia/libs/voice/streams/PCMString.lua create mode 100644 deps/discordia/package.lua create mode 100644 deps/http-codec.lua create mode 100644 deps/pathjoin.lua create mode 100644 deps/resource.lua create mode 100644 deps/secure-socket/biowrap.lua create mode 100644 deps/secure-socket/context.lua create mode 100644 deps/secure-socket/init.lua create mode 100644 deps/secure-socket/package.lua create mode 100644 deps/secure-socket/root_ca.dat create mode 100644 deps/sha1.lua create mode 100644 deps/sqlite3.lua create mode 100644 deps/websocket-codec.lua create mode 100644 libraries/.tasklib.lua.swp create mode 100644 libraries/air.lua create mode 120000 libraries/classes/.test/baseclass.lua create mode 100644 libraries/classes/.test/test.lua create mode 120000 libraries/classes/.test/tty-colors.lua create mode 100644 libraries/classes/acl.lua create mode 100644 libraries/classes/baseclass.lua create mode 100644 libraries/classes/command-handler.lua create mode 100644 libraries/classes/command.lua create mode 100644 libraries/classes/emitter-proxy.lua create mode 100644 libraries/classes/interfaces/discord.lua create mode 100644 libraries/classes/monitor.lua create mode 100644 libraries/classes/plugin-handler.lua create mode 100644 libraries/classes/plugin.lua create mode 100644 libraries/classes/server-handler.lua create mode 100644 libraries/esolangs/befunge.lua create mode 100644 libraries/esolangs/brainfuck.lua create mode 100644 libraries/eventlist.lua create mode 100644 libraries/file.lua create mode 100644 libraries/import.lua create mode 100644 libraries/markov.lua create mode 100644 libraries/purify.lua create mode 100644 libraries/table-utils.lua create mode 100644 libraries/tasklib.lua create mode 100644 libraries/unixToString.lua create mode 100644 plugins/debug/init.lua create mode 100644 plugins/esolang/help.lua create mode 100644 plugins/esolang/init.lua create mode 100644 plugins/help/init.lua create mode 100644 plugins/meta/help.lua create mode 100644 plugins/meta/init.lua create mode 100644 plugins/plugins/init.lua create mode 100644 plugins/tools/init.lua create mode 100644 resources/default.json create mode 100644 resources/freud.json create mode 100644 resources/markov_preset_generator/markov.lua create mode 100644 resources/markov_preset_generator/preset_generator.lua create mode 100644 resources/reddit.json create mode 100644 resources/travisscott.json create mode 100644 todo-stuff/enforcer/init.lua create mode 100644 todo-stuff/reactions/init.lua create mode 100644 todo-stuff/settings/init.lua create mode 100644 todo-stuff/tasks/init.lua diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9fd3707 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/token diff --git a/bot.lua b/bot.lua new file mode 100644 index 0000000..13948b0 --- /dev/null +++ b/bot.lua @@ -0,0 +1,33 @@ +package.path = "./libraries/?.lua;./libraries/?/init.lua;"..package.path + +--load discordia +discordia = require("discordia") +client = discordia.Client() + +--activate the import system +local import = require("import")(require) + +--create server +local server = import("classes.server-handler") +client:on("ready",function() + print("starting test") + local new_server = server(client,client:getGuild("640251445949759499"),{ + autosave_frequency = 20, + default_plugins = { + "meta", + "help", + "plugins", + "esolang", + "tools" + } + }) +end) + +--load token +local tempfile = io.open("./token","r") +if not tempfile then + error("./token file does not exist") +end +local nstr = tempfile:read("*l") +tempfile:close() +client:run('Bot '..nstr) diff --git a/deps/base64.lua b/deps/base64.lua new file mode 100644 index 0000000..abe3191 --- /dev/null +++ b/deps/base64.lua @@ -0,0 +1,114 @@ +--[[lit-meta + name = "creationix/base64" + description = "A pure lua implemention of base64 using bitop" + tags = {"crypto", "base64", "bitop"} + version = "2.0.0" + license = "MIT" + author = { name = "Tim Caswell" } +]] + +local bit = require 'bit' +local rshift = bit.rshift +local lshift = bit.lshift +local bor = bit.bor +local band = bit.band +local char = string.char +local byte = string.byte +local concat = table.concat +local codes = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' + +-- Loop over input 3 bytes at a time +-- a,b,c are 3 x 8-bit numbers +-- they are encoded into groups of 4 x 6-bit numbers +-- aaaaaa aabbbb bbbbcc cccccc +-- if there is no c, then pad the 4th with = +-- if there is also no b then pad the 3rd with = +local function base64Encode(str) + local parts = {} + local j = 1 + for i = 1, #str, 3 do + local a, b, c = byte(str, i, i + 2) + parts[j] = char( + -- Higher 6 bits of a + byte(codes, rshift(a, 2) + 1), + -- Lower 2 bits of a + high 4 bits of b + byte(codes, bor( + lshift(band(a, 3), 4), + b and rshift(b, 4) or 0 + ) + 1), + -- Low 4 bits of b + High 2 bits of c + b and byte(codes, bor( + lshift(band(b, 15), 2), + c and rshift(c, 6) or 0 + ) + 1) or 61, -- 61 is '=' + -- Lower 6 bits of c + c and byte(codes, band(c, 63) + 1) or 61 -- 61 is '=' + ) + j = j + 1 + end + return concat(parts) +end + +-- Reverse map from character code to 6-bit integer +local map = {} +for i = 1, #codes do + map[byte(codes, i)] = i - 1 +end + +-- loop over input 4 characters at a time +-- The characters are mapped to 4 x 6-bit integers a,b,c,d +-- They need to be reassalbled into 3 x 8-bit bytes +-- aaaaaabb bbbbcccc ccdddddd +-- if d is padding then there is no 3rd byte +-- if c is padding then there is no 2nd byte +local function base64Decode(data) + local bytes = {} + local j = 1 + for i = 1, #data, 4 do + local a = map[byte(data, i)] + local b = map[byte(data, i + 1)] + local c = map[byte(data, i + 2)] + local d = map[byte(data, i + 3)] + + -- higher 6 bits are the first char + -- lower 2 bits are upper 2 bits of second char + bytes[j] = char(bor(lshift(a, 2), rshift(b, 4))) + + -- if the third char is not padding, we have a second byte + if c < 64 then + -- high 4 bits come from lower 4 bits in b + -- low 4 bits come from high 4 bits in c + bytes[j + 1] = char(bor(lshift(band(b, 0xf), 4), rshift(c, 2))) + + -- if the fourth char is not padding, we have a third byte + if d < 64 then + -- Upper 2 bits come from Lower 2 bits of c + -- Lower 6 bits come from d + bytes[j + 2] = char(bor(lshift(band(c, 3), 6), d)) + end + end + j = j + 3 + end + return concat(bytes) +end + +assert(base64Encode("") == "") +assert(base64Encode("f") == "Zg==") +assert(base64Encode("fo") == "Zm8=") +assert(base64Encode("foo") == "Zm9v") +assert(base64Encode("foob") == "Zm9vYg==") +assert(base64Encode("fooba") == "Zm9vYmE=") +assert(base64Encode("foobar") == "Zm9vYmFy") + +assert(base64Decode("") == "") +assert(base64Decode("Zg==") == "f") +assert(base64Decode("Zm8=") == "fo") +assert(base64Decode("Zm9v") == "foo") +assert(base64Decode("Zm9vYg==") == "foob") +assert(base64Decode("Zm9vYmE=") == "fooba") +assert(base64Decode("Zm9vYmFy") == "foobar") + +return { + encode = base64Encode, + decode = base64Decode, +} diff --git a/deps/coro-channel.lua b/deps/coro-channel.lua new file mode 100644 index 0000000..15f7fcb --- /dev/null +++ b/deps/coro-channel.lua @@ -0,0 +1,183 @@ +--[[lit-meta + name = "creationix/coro-channel" + version = "3.0.1" + homepage = "https://github.com/luvit/lit/blob/master/deps/coro-channel.lua" + description = "An adapter for wrapping uv streams as coro-streams." + tags = {"coro", "adapter"} + license = "MIT" + author = { name = "Tim Caswell" } +]] + +-- local p = require('pretty-print').prettyPrint + +local function makeCloser(socket) + local closer = { + read = false, + written = false, + errored = false, + } + + local closed = false + + local function close() + if closed then return end + closed = true + if not closer.readClosed then + closer.readClosed = true + if closer.onClose then + closer.onClose() + end + end + if not socket:is_closing() then + socket:close() + end + end + + closer.close = close + + function closer.check() + if closer.errored or (closer.read and closer.written) then + return close() + end + end + + return closer +end + +local function makeRead(socket, closer) + local paused = true + + local queue = {} + local tindex = 0 + local dindex = 0 + + local function dispatch(data) + + -- p("<-", data[1]) + + if tindex > dindex then + local thread = queue[dindex] + queue[dindex] = nil + dindex = dindex + 1 + assert(coroutine.resume(thread, unpack(data))) + else + queue[dindex] = data + dindex = dindex + 1 + if not paused then + paused = true + assert(socket:read_stop()) + end + end + end + + closer.onClose = function () + if not closer.read then + closer.read = true + return dispatch {nil, closer.errored} + end + end + + local function onRead(err, chunk) + if err then + closer.errored = err + return closer.check() + end + if not chunk then + if closer.read then return end + closer.read = true + dispatch {} + return closer.check() + end + return dispatch {chunk} + end + + local function read() + if dindex > tindex then + local data = queue[tindex] + queue[tindex] = nil + tindex = tindex + 1 + return unpack(data) + end + if paused then + paused = false + assert(socket:read_start(onRead)) + end + queue[tindex] = coroutine.running() + tindex = tindex + 1 + return coroutine.yield() + end + + -- Auto use wrapper library for backwards compat + return read +end + +local function makeWrite(socket, closer) + + local function wait() + local thread = coroutine.running() + return function (err) + assert(coroutine.resume(thread, err)) + end + end + + local function write(chunk) + if closer.written then + return nil, "already shutdown" + end + + -- p("->", chunk) + + if chunk == nil then + closer.written = true + closer.check() + local success, err = socket:shutdown(wait()) + if not success then + return nil, err + end + err = coroutine.yield() + return not err, err + end + + local success, err = socket:write(chunk, wait()) + if not success then + closer.errored = err + closer.check() + return nil, err + end + err = coroutine.yield() + return not err, err + end + + return write +end + +local function wrapRead(socket) + local closer = makeCloser(socket) + closer.written = true + return makeRead(socket, closer), closer.close +end + +local function wrapWrite(socket) + local closer = makeCloser(socket) + closer.read = true + return makeWrite(socket, closer), closer.close +end + +local function wrapStream(socket) + assert(socket + and socket.write + and socket.shutdown + and socket.read_start + and socket.read_stop + and socket.is_closing + and socket.close, "socket does not appear to be a socket/uv_stream_t") + + local closer = makeCloser(socket) + return makeRead(socket, closer), makeWrite(socket, closer), closer.close +end + +return { + wrapRead = wrapRead, + wrapWrite = wrapWrite, + wrapStream = wrapStream, +} diff --git a/deps/coro-http.lua b/deps/coro-http.lua new file mode 100644 index 0000000..0e0ef3f --- /dev/null +++ b/deps/coro-http.lua @@ -0,0 +1,195 @@ +--[[lit-meta + name = "creationix/coro-http" + version = "3.1.0" + dependencies = { + "creationix/coro-net@3.0.0", + "luvit/http-codec@3.0.0" + } + homepage = "https://github.com/luvit/lit/blob/master/deps/coro-http.lua" + description = "An coro style http(s) client and server helper." + tags = {"coro", "http"} + license = "MIT" + author = { name = "Tim Caswell" } +]] + +local httpCodec = require('http-codec') +local net = require('coro-net') + +local function createServer(host, port, onConnect) + net.createServer({ + host = host, + port = port, + encode = httpCodec.encoder(), + decode = httpCodec.decoder(), + }, function (read, write, socket) + for head in read do + local parts = {} + for part in read do + if #part > 0 then + parts[#parts + 1] = part + else + break + end + end + local body = table.concat(parts) + head, body = onConnect(head, body, socket) + write(head) + if body then write(body) end + write("") + if not head.keepAlive then break end + end + end) +end + +local function parseUrl(url) + local protocol, host, hostname, port, path = url:match("^(https?:)//(([^/:]+):?([0-9]*))(/?.*)$") + if not protocol then error("Not a valid http url: " .. url) end + local tls = protocol == "https:" + port = port and tonumber(port) or (tls and 443 or 80) + if path == "" then path = "/" end + return { + tls = tls, + host = host, + hostname = hostname, + port = port, + path = path + } +end + +local connections = {} + +local function getConnection(host, port, tls, timeout) + for i = #connections, 1, -1 do + local connection = connections[i] + if connection.host == host and connection.port == port and connection.tls == tls then + table.remove(connections, i) + -- Make sure the connection is still alive before reusing it. + if not connection.socket:is_closing() then + connection.reused = true + connection.socket:ref() + return connection + end + end + end + local read, write, socket, updateDecoder, updateEncoder = assert(net.connect { + host = host, + port = port, + tls = tls, + timeout = timeout, + encode = httpCodec.encoder(), + decode = httpCodec.decoder() + }) + return { + socket = socket, + host = host, + port = port, + tls = tls, + read = read, + write = write, + updateEncoder = updateEncoder, + updateDecoder = updateDecoder, + reset = function () + -- This is called after parsing the response head from a HEAD request. + -- If you forget, the codec might hang waiting for a body that doesn't exist. + updateDecoder(httpCodec.decoder()) + end + } +end + +local function saveConnection(connection) + if connection.socket:is_closing() then return end + connections[#connections + 1] = connection + connection.socket:unref() +end + +local function request(method, url, headers, body, timeout) + local uri = parseUrl(url) + local connection = getConnection(uri.hostname, uri.port, uri.tls, timeout) + local read = connection.read + local write = connection.write + + local req = { + method = method, + path = uri.path, + {"Host", uri.host} + } + local contentLength + local chunked + if headers then + for i = 1, #headers do + local key, value = unpack(headers[i]) + key = key:lower() + if key == "content-length" then + contentLength = value + elseif key == "content-encoding" and value:lower() == "chunked" then + chunked = true + end + req[#req + 1] = headers[i] + end + end + + if type(body) == "string" then + if not chunked and not contentLength then + req[#req + 1] = {"Content-Length", #body} + end + end + + write(req) + if body then write(body) end + local res = read() + if not res then + if not connection.socket:is_closing() then + connection.socket:close() + end + -- If we get an immediate close on a reused socket, try again with a new socket. + -- TODO: think about if this could resend requests with side effects and cause + -- them to double execute in the remote server. + if connection.reused then + return request(method, url, headers, body) + end + error("Connection closed") + end + + body = {} + if req.method == "HEAD" then + connection.reset() + else + while true do + local item = read() + if not item then + res.keepAlive = false + break + end + if #item == 0 then + break + end + body[#body + 1] = item + end + end + + if res.keepAlive then + saveConnection(connection) + else + write() + end + + -- Follow redirects + if method == "GET" and (res.code == 302 or res.code == 307) then + for i = 1, #res do + local key, location = unpack(res[i]) + if key:lower() == "location" then + return request(method, location, headers) + end + end + end + + return res, table.concat(body) +end + +return { + createServer = createServer, + parseUrl = parseUrl, + getConnection = getConnection, + saveConnection = saveConnection, + request = request, +} diff --git a/deps/coro-net.lua b/deps/coro-net.lua new file mode 100644 index 0000000..1871452 --- /dev/null +++ b/deps/coro-net.lua @@ -0,0 +1,181 @@ +--[[lit-meta + name = "creationix/coro-net" + version = "3.2.0" + dependencies = { + "creationix/coro-channel@3.0.0", + "creationix/coro-wrapper@3.0.0", + } + optionalDependencies = { + "luvit/secure-socket@1.0.0" + } + homepage = "https://github.com/luvit/lit/blob/master/deps/coro-net.lua" + description = "An coro style client and server helper for tcp and pipes." + tags = {"coro", "tcp", "pipe", "net"} + license = "MIT" + author = { name = "Tim Caswell" } +]] + +local uv = require('uv') +local wrapStream = require('coro-channel').wrapStream +local wrapper = require('coro-wrapper') +local merger = wrapper.merger +local decoder = wrapper.decoder +local encoder = wrapper.encoder +local secureSocket -- Lazy required from "secure-socket" on first use. + +local function makeCallback(timeout) + local thread = coroutine.running() + local timer, done + if timeout then + timer = uv.new_timer() + timer:start(timeout, 0, function () + if done then return end + done = true + timer:close() + return assert(coroutine.resume(thread, nil, "timeout")) + end) + end + return function (err, data) + if done then return end + done = true + if timer then timer:close() end + if err then + return assert(coroutine.resume(thread, nil, err)) + end + return assert(coroutine.resume(thread, data or true)) + end +end + +local function normalize(options, server) + local t = type(options) + if t == "string" then + options = {path=options} + elseif t == "number" then + options = {port=options} + elseif t ~= "table" then + assert("Net options must be table, string, or number") + end + if options.port or options.host then + options.isTcp = true + options.host = options.host or "127.0.0.1" + assert(options.port, "options.port is required for tcp connections") + elseif options.path then + options.isTcp = false + else + error("Must set either options.path or options.port") + end + if options.tls == true then + options.tls = {} + end + if options.tls then + if server then + options.tls.server = true + assert(options.tls.cert, "TLS servers require a certificate") + assert(options.tls.key, "TLS servers require a key") + else + options.tls.server = false + options.tls.servername = options.host + end + end + return options +end + +local function connect(options) + local socket, success, err + options = normalize(options) + if options.isTcp then + success, err = uv.getaddrinfo(options.host, options.port, { + socktype = options.socktype or "stream", + family = options.family or "inet", + }, makeCallback(options.timeout)) + if not success then return nil, err end + local res + res, err = coroutine.yield() + if not res then return nil, err end + socket = uv.new_tcp() + socket:connect(res[1].addr, res[1].port, makeCallback(options.timeout)) + else + socket = uv.new_pipe(false) + socket:connect(options.path, makeCallback(options.timeout)) + end + success, err = coroutine.yield() + if not success then return nil, err end + local dsocket + if options.tls then + if not secureSocket then secureSocket = require('secure-socket') end + dsocket, err = secureSocket(socket, options.tls) + if not dsocket then + return nil, err + end + else + dsocket = socket + end + + local read, write, close = wrapStream(dsocket) + local updateDecoder, updateEncoder + if options.scan then + -- TODO: Should we expose updateScan somehow? + read = merger(read, options.scan) + end + if options.decode then + read, updateDecoder = decoder(read, options.decode) + end + if options.encode then + write, updateEncoder = encoder(write, options.encode) + end + return read, write, dsocket, updateDecoder, updateEncoder, close +end + +local function createServer(options, onConnect) + local server + options = normalize(options, true) + if options.isTcp then + server = uv.new_tcp() + assert(server:bind(options.host, options.port)) + else + server = uv.new_pipe(false) + assert(server:bind(options.path)) + end + assert(server:listen(256, function (err) + assert(not err, err) + local socket = options.isTcp and uv.new_tcp() or uv.new_pipe(false) + server:accept(socket) + coroutine.wrap(function () + local success, failure = xpcall(function () + local dsocket + if options.tls then + if not secureSocket then secureSocket = require('secure-socket') end + dsocket = assert(secureSocket(socket, options.tls)) + dsocket.socket = socket + else + dsocket = socket + end + + local read, write = wrapStream(dsocket) + local updateDecoder, updateEncoder + if options.scan then + -- TODO: should we expose updateScan somehow? + read = merger(read, options.scan) + end + if options.decode then + read, updateDecoder = decoder(read, options.decode) + end + if options.encode then + write, updateEncoder = encoder(write, options.encode) + end + + return onConnect(read, write, dsocket, updateDecoder, updateEncoder) + end, debug.traceback) + if not success then + print(failure) + end + end)() + end)) + return server +end + +return { + makeCallback = makeCallback, + connect = connect, + createServer = createServer, +} diff --git a/deps/coro-websocket.lua b/deps/coro-websocket.lua new file mode 100644 index 0000000..50eee9e --- /dev/null +++ b/deps/coro-websocket.lua @@ -0,0 +1,196 @@ +--[[lit-meta + name = "creationix/coro-websocket" + version = "3.1.0" + dependencies = { + "luvit/http-codec@3.0.0", + "creationix/websocket-codec@3.0.0", + "creationix/coro-net@3.0.0", + } + homepage = "https://github.com/luvit/lit/blob/master/deps/coro-websocket.lua" + description = "Websocket helpers assuming coro style I/O." + tags = {"coro", "websocket"} + license = "MIT" + author = { name = "Tim Caswell" } +]] + +local uv = require('uv') +local httpCodec = require('http-codec') +local websocketCodec = require('websocket-codec') +local net = require('coro-net') + +local function parseUrl(url) + local protocol, host, port, pathname = string.match(url, "^(wss?)://([^:/]+):?(%d*)(/?[^#?]*)") + local tls + if protocol == "ws" then + port = tonumber(port) or 80 + tls = false + elseif protocol == "wss" then + port = tonumber(port) or 443 + tls = true + else + return nil, "Sorry, only ws:// or wss:// protocols supported" + end + return { + host = host, + port = port, + tls = tls, + pathname = pathname + } +end + +local function wrapIo(rawRead, rawWrite, options) + + local closeSent = false + + local timer + + local function cleanup() + if timer then + if not timer:is_closing() then + timer:close() + end + timer = nil + end + end + + local function write(message) + if message then + message.mask = options.mask + if message.opcode == 8 then + closeSent = true + rawWrite(message) + cleanup() + return rawWrite() + end + else + if not closeSent then + return write({ + opcode = 8, + payload = "" + }) + end + end + return rawWrite(message) + end + + + local function read() + while true do + local message = rawRead() + if not message then + return cleanup() + end + if message.opcode < 8 then + return message + end + if not closeSent then + if message.opcode == 8 then + write { + opcode = 8, + payload = message.payload + } + elseif message.opcode == 9 then + write { + opcode = 10, + payload = message.payload + } + end + return message + end + end + end + + if options.heartbeat then + local interval = options.heartbeat + timer = uv.new_timer() + timer:unref() + timer:start(interval, interval, function () + coroutine.wrap(function () + local success, err = write { + opcode = 10, + payload = "" + } + if not success then + timer:close() + print(err) + end + end)() + end) + end + + return read, write +end + +-- options table to configure connection +-- options.path +-- options.host +-- options.port +-- options.tls +-- options.pathname +-- options.subprotocol +-- options.headers (as list of header/value pairs) +-- options.timeout +-- options.heartbeat +-- returns res, read, write (res.socket has socket) +local function connect(options) + options = options or {} + local config = { + path = options.path, + host = options.host, + port = options.port, + tls = options.tls, + encode = httpCodec.encoder(), + decode = httpCodec.decoder(), + } + local read, write, socket, updateDecoder, updateEncoder + = net.connect(config, options.timeout or 10000) + if not read then + return nil, write + end + + local res + + local success, err = websocketCodec.handshake({ + host = options.host, + path = options.pathname, + protocol = options.subprotocol + }, function (req) + local headers = options.headers + if headers then + for i = 1, #headers do + req[#req + 1] = headers[i] + end + end + write(req) + res = read() + if not res then error("Missing server response") end + if res.code == 400 then + -- p { req = req, res = res } + local reason = read() or res.reason + error("Invalid request: " .. reason) + end + return res + end) + if not success then + return nil, err + end + + -- Upgrade the protocol to websocket + updateDecoder(websocketCodec.decode) + updateEncoder(websocketCodec.encode) + + read, write = wrapIo(read, write, { + mask = true, + heartbeat = options.heartbeat + }) + + res.socket = socket + return res, read, write + +end + +return { + parseUrl = parseUrl, + wrapIo = wrapIo, + connect = connect, +} diff --git a/deps/coro-wrapper.lua b/deps/coro-wrapper.lua new file mode 100644 index 0000000..c43671e --- /dev/null +++ b/deps/coro-wrapper.lua @@ -0,0 +1,151 @@ +--[[lit-meta + name = "creationix/coro-wrapper" + version = "3.1.0" + homepage = "https://github.com/luvit/lit/blob/master/deps/coro-wrapper.lua" + description = "An adapter for applying decoders to coro-streams." + tags = {"coro", "decoder", "adapter"} + license = "MIT" + author = { name = "Tim Caswell" } +]] + +local concat = table.concat +local sub = string.sub + +-- Merger allows for effecient merging of many chunks. +-- The scan function returns truthy when the chunk contains a useful delimeter +-- Or in other words, when there is enough data to flush to the decoder. +-- merger(read, scan) -> read, updateScan +-- read() -> chunk or nil +-- scan(chunk) -> should_flush +-- updateScan(scan) +local function merger(read, scan) + local parts = {} + + -- Return a new read function that combines chunks smartly + return function () + + while true do + -- Read the next event from upstream. + local chunk = read() + + -- We got an EOS (end of stream) + if not chunk then + -- If there is nothing left to flush, emit EOS here. + if #parts == 0 then return end + + -- Flush the buffer + chunk = concat(parts) + parts = {} + return chunk + end + + -- Accumulate the chunk + parts[#parts + 1] = chunk + + -- Flush the buffer if scan tells us to. + if scan(chunk) then + chunk = concat(parts) + parts = {} + return chunk + end + + end + end, + + -- This is used to update or disable the scan function. It's useful for + -- protocols that change mid-stream (like HTTP upgrades in websockets) + function (newScan) + scan = newScan + end +end + +-- Decoder takes in a read function and a decode function and returns a new +-- read function that emits decoded events. When decode returns `nil` it means +-- that it needs more data before it can parse. The index output in decode is +-- the index to start the next decode. If output index if nil it means nothing +-- is leftover and next decode starts fresh. +-- decoder(read, decode) -> read, updateDecode +-- read() -> chunk or nil +-- decode(chunk, index) -> nil or (data, index) +-- updateDecode(Decode) +local function decoder(read, decode) + local buffer, index + local want = true + return function () + + while true do + -- If there isn't enough data to decode then get more data. + if want then + local chunk = read() + if buffer then + -- If we had leftover data in the old buffer, trim it down. + if index > 1 then + buffer = sub(buffer, index) + index = 1 + end + if chunk then + -- Concatenate the chunk with the old data + buffer = buffer .. chunk + end + else + -- If there was no leftover data, set new data in the buffer + if chunk then + buffer = chunk + index = 1 + else + buffer = nil + index = nil + end + end + end + + -- Return nil if the buffer is empty + if buffer == '' or buffer == nil then + return nil + end + + -- If we have data, lets try to decode it + local item, newIndex = decode(buffer, index) + + want = not newIndex + if item or newIndex then + -- There was enough data to emit an event! + if newIndex then + assert(type(newIndex) == "number", "index must be a number if set") + -- There was leftover data + index = newIndex + else + want = true + -- There was no leftover data + buffer = nil + index = nil + end + -- Emit the event + return item + end + + + end + end, + function (newDecode) + decode = newDecode + end +end + +local function encoder(write, encode) + return function (item) + if not item then + return write() + end + return write(encode(item)) + end, + function (newEncode) + encode = newEncode + end +end + +return { + merger = merger, + decoder = decoder, + encoder = encoder, +} diff --git a/deps/discordia/docgen.lua b/deps/discordia/docgen.lua new file mode 100644 index 0000000..fd1dc48 --- /dev/null +++ b/deps/discordia/docgen.lua @@ -0,0 +1,373 @@ +--[=[ +@c ClassName [x base_1 x base_2 ... x base_n] +@t tag +@mt methodTag (applies to all class methods) +@p parameterName type +@op optionalParameterName type +@d description+ +]=] + +--[=[ +@m methodName +@t tag +@p parameterName type +@op optionalParameterName type +@r return +@d description+ +]=] + +--[=[ +@p propertyName type description+ +]=] + +local fs = require('fs') +local pathjoin = require('pathjoin') + +local insert, sort, concat = table.insert, table.sort, table.concat +local format = string.format +local pathJoin = pathjoin.pathJoin + +local function scan(dir) + for fileName, fileType in fs.scandirSync(dir) do + local path = pathJoin(dir, fileName) + if fileType == 'file' then + coroutine.yield(path) + else + scan(path) + end + end +end + +local function match(s, pattern) -- only useful for one capture + return assert(s:match(pattern), s) +end + +local function gmatch(s, pattern, hash) -- only useful for one capture + local tbl = {} + if hash then + for k in s:gmatch(pattern) do + tbl[k] = true + end + else + for v in s:gmatch(pattern) do + insert(tbl, v) + end + end + return tbl +end + +local function matchType(s) + return s:match('^@(%S+)') +end + +local function matchComments(s) + return s:gmatch('--%[=%[%s*(.-)%s*%]=%]') +end + +local function matchClassName(s) + return match(s, '@c (%S+)') +end + +local function matchMethodName(s) + return match(s, '@m (%S+)') +end + +local function matchDescription(s) + return match(s, '@d (.+)'):gsub('%s+', ' ') +end + +local function matchParents(s) + return gmatch(s, 'x (%S+)') +end + +local function matchReturns(s) + return gmatch(s, '@r (%S+)') +end + +local function matchTags(s) + return gmatch(s, '@t (%S+)', true) +end + +local function matchMethodTags(s) + return gmatch(s, '@mt (%S+)', true) +end + +local function matchProperty(s) + local a, b, c = s:match('@p (%S+) (%S+) (.+)') + return { + name = assert(a, s), + type = assert(b, s), + desc = assert(c, s):gsub('%s+', ' '), + } +end + +local function matchParameters(s) + local ret = {} + for optional, paramName, paramType in s:gmatch('@(o?)p (%S+) (%S+)') do + insert(ret, {paramName, paramType, optional == 'o'}) + end + return ret +end + +local function matchMethod(s) + return { + name = matchMethodName(s), + desc = matchDescription(s), + parameters = matchParameters(s), + returns = matchReturns(s), + tags = matchTags(s), + } +end + +---- + +local docs = {} + +local function newClass() + + local class = { + methods = {}, + statics = {}, + properties = {}, + } + + local function init(s) + class.name = matchClassName(s) + class.parents = matchParents(s) + class.desc = matchDescription(s) + class.parameters = matchParameters(s) + class.tags = matchTags(s) + class.methodTags = matchMethodTags(s) + assert(not docs[class.name], 'duplicate class: ' .. class.name) + docs[class.name] = class + end + + return class, init + +end + +for f in coroutine.wrap(scan), './libs' do + + local d = assert(fs.readFileSync(f)) + + local class, initClass = newClass() + for s in matchComments(d) do + local t = matchType(s) + if t == 'c' then + initClass(s) + elseif t == 'm' then + local method = matchMethod(s) + for k, v in pairs(class.methodTags) do + method.tags[k] = v + end + method.class = class + insert(method.tags.static and class.statics or class.methods, method) + elseif t == 'p' then + insert(class.properties, matchProperty(s)) + end + end + +end + +---- + +local output = 'docs' + +local function link(str) + if type(str) == 'table' then + local ret = {} + for i, v in ipairs(str) do + ret[i] = link(v) + end + return concat(ret, ', ') + else + local ret = {} + for t in str:gmatch('[^/]+') do + insert(ret, docs[t] and format('[[%s]]', t) or t) + end + return concat(ret, '/') + end +end + +local function sorter(a, b) + return a.name < b.name +end + +local function writeHeading(f, heading) + f:write('## ', heading, '\n\n') +end + +local function writeProperties(f, properties) + sort(properties, sorter) + f:write('| Name | Type | Description |\n') + f:write('|-|-|-|\n') + for _, v in ipairs(properties) do + f:write('| ', v.name, ' | ', link(v.type), ' | ', v.desc, ' |\n') + end + f:write('\n') +end + +local function writeParameters(f, parameters) + f:write('(') + local optional + if #parameters > 0 then + for i, param in ipairs(parameters) do + f:write(param[1]) + if i < #parameters then + f:write(', ') + end + if param[3] then + optional = true + end + end + f:write(')\n\n') + if optional then + f:write('| Parameter | Type | Optional |\n') + f:write('|-|-|:-:|\n') + for _, param in ipairs(parameters) do + local o = param[3] and '✔' or '' + f:write('| ', param[1], ' | ', link(param[2]), ' | ', o, ' |\n') + end + f:write('\n') + else + f:write('| Parameter | Type |\n') + f:write('|-|-|\n') + for _, param in ipairs(parameters) do + f:write('| ', param[1], ' | ', link(param[2]), ' |\n') + end + f:write('\n') + end + else + f:write(')\n\n') + end +end + +local methodTags = {} + +methodTags['http'] = 'This method always makes an HTTP request.' +methodTags['http?'] = 'This method may make an HTTP request.' +methodTags['ws'] = 'This method always makes a WebSocket request.' +methodTags['mem'] = 'This method only operates on data in memory.' + +local function checkTags(tbl, check) + for i, v in ipairs(check) do + if tbl[v] then + for j, w in ipairs(check) do + if i ~= j then + if tbl[w] then + return error(string.format('mutually exclusive tags encountered: %s and %s', v, w), 1) + end + end + end + end + end +end + +local function writeMethods(f, methods) + + sort(methods, sorter) + for _, method in ipairs(methods) do + + f:write('### ', method.name) + writeParameters(f, method.parameters) + f:write(method.desc, '\n\n') + + local tags = method.tags + checkTags(tags, {'http', 'http?', 'mem'}) + checkTags(tags, {'ws', 'mem'}) + + for k in pairs(tags) do + if k ~= 'static' then + assert(methodTags[k], k) + f:write('*', methodTags[k], '*\n\n') + end + end + + f:write('**Returns:** ', link(method.returns), '\n\n----\n\n') + + end + +end + +if not fs.existsSync(output) then + fs.mkdirSync(output) +end + +local function collectParents(parents, k, ret, seen) + ret = ret or {} + seen = seen or {} + for _, parent in ipairs(parents) do + parent = docs[parent] + if parent then + for _, v in ipairs(parent[k]) do + if not seen[v] then + seen[v] = true + insert(ret, v) + end + end + end + collectParents(parent.parents, k, ret, seen) + end + return ret +end + +for _, class in pairs(docs) do + + local f = io.open(pathJoin(output, class.name .. '.md'), 'w') + + local parents = class.parents + local parentLinks = link(parents) + + if next(parents) then + f:write('#### *extends ', parentLinks, '*\n\n') + end + + f:write(class.desc, '\n\n') + + checkTags(class.tags, {'ui', 'abc'}) + if class.tags.ui then + writeHeading(f, 'Constructor') + f:write('### ', class.name) + writeParameters(f, class.parameters) + elseif class.tags.abc then + f:write('*This is an abstract base class. Direct instances should never exist.*\n\n') + else + f:write('*Instances of this class should not be constructed by users.*\n\n') + end + + local properties = collectParents(parents, 'properties') + if next(properties) then + writeHeading(f, 'Properties Inherited From ' .. parentLinks) + writeProperties(f, properties) + end + + if next(class.properties) then + writeHeading(f, 'Properties') + writeProperties(f, class.properties) + end + + local statics = collectParents(parents, 'statics') + if next(statics) then + writeHeading(f, 'Static Methods Inherited From ' .. parentLinks) + writeMethods(f, statics) + end + + local methods = collectParents(parents, 'methods') + if next(methods) then + writeHeading(f, 'Methods Inherited From ' .. parentLinks) + writeMethods(f, methods) + end + + if next(class.statics) then + writeHeading(f, 'Static Methods') + writeMethods(f, class.statics) + end + + if next(class.methods) then + writeHeading(f, 'Methods') + writeMethods(f, class.methods) + end + + f:close() + +end diff --git a/deps/discordia/examples/appender.lua b/deps/discordia/examples/appender.lua new file mode 100644 index 0000000..291d85c --- /dev/null +++ b/deps/discordia/examples/appender.lua @@ -0,0 +1,28 @@ +local discordia = require("discordia") +local client = discordia.Client() + +local lines = {} -- blank table of messages + +client:on("ready", function() -- bot is ready + print("Logged in as " .. client.user.username) +end) + +client:on("messageCreate", function(message) + + local content = message.content + local author = message.author + + if author == client.user then return end -- the bot should not append its own messages + + if content == "!lines" then -- if the lines command is activated + message.channel:send { + file = {"lines.txt", table.concat(lines, "\n")} -- concatenate and send the collected lines in a file + } + lines = {} -- empty the lines table + else -- if the lines command is NOT activated + table.insert(lines, content) -- append the message as a new line + end + +end) + +client:run("Bot BOT_TOKEN") -- replace BOT_TOKEN with your bot token diff --git a/deps/discordia/examples/basicCommands.lua b/deps/discordia/examples/basicCommands.lua new file mode 100644 index 0000000..57498e9 --- /dev/null +++ b/deps/discordia/examples/basicCommands.lua @@ -0,0 +1,25 @@ +local discordia = require("discordia") +local client = discordia.Client() + +discordia.extensions() -- load all helpful extensions + +client:on("ready", function() -- bot is ready + print("Logged in as " .. client.user.username) +end) + +client:on("messageCreate", function(message) + + local content = message.content + local args = content:split(" ") -- split all arguments into a table + + if args[1] == "!ping" then + message:reply("Pong!") + elseif args[1] == "!echo" then + table.remove(args, 1) -- remove the first argument (!echo) from the table + message:reply(table.concat(args, " ")) -- concatenate the arguments into a string, then reply with it + end + +end) + + +client:run("Bot BOT_TOKEN") -- replace BOT_TOKEN with your bot token diff --git a/deps/discordia/examples/embed.lua b/deps/discordia/examples/embed.lua new file mode 100644 index 0000000..3f546f6 --- /dev/null +++ b/deps/discordia/examples/embed.lua @@ -0,0 +1,45 @@ +local discordia = require("discordia") +local client = discordia.Client() + + +client:on("ready", function() -- bot is ready + print("Logged in as " .. client.user.username) +end) + +client:on("messageCreate", function(message) + + local content = message.content + local author = message.author + + if content == "!embed" then + message:reply { + embed = { + title = "Embed Title", + description = "Here is my fancy description!", + author = { + name = author.username, + icon_url = author.avatarURL + }, + fields = { -- array of fields + { + name = "Field 1", + value = "This is some information", + inline = true + }, + { + name = "Field 2", + value = "This is some more information", + inline = false + } + }, + footer = { + text = "Created with Discordia" + }, + color = 0x000000 -- hex color code + } + } + end + +end) + +client:run("Bot BOT_TOKEN") -- replace BOT_TOKEN with your bot token diff --git a/deps/discordia/examples/helpCommandExample.lua b/deps/discordia/examples/helpCommandExample.lua new file mode 100644 index 0000000..e22a3e6 --- /dev/null +++ b/deps/discordia/examples/helpCommandExample.lua @@ -0,0 +1,44 @@ +local discordia = require('discordia') +local client = discordia.Client() +discordia.extensions() -- load all helpful extensions + +local prefix = "." +local commands = { + [prefix .. "ping"] = { + description = "Answers with pong.", + exec = function(message) + message.channel:send("Pong!") + end + }, + [prefix .. "hello"] = { + description = "Answers with world.", + exec = function(message) + message.channel:send("world!") + end + } +} + +client:on('ready', function() + p(string.format('Logged in as %s', client.user.username)) +end) + +client:on("messageCreate", function(message) + local args = message.content:split(" ") -- split all arguments into a table + + local command = commands[args[1]] + if command then -- ping or hello + command.exec(message) -- execute the command + end + + if args[1] == prefix.."help" then -- display all the commands + local output = {} + for word, tbl in pairs(commands) do + table.insert(output, "Command: " .. word .. "\nDescription: " .. tbl.description) + end + + message:reply(table.concat(output, "\n\n")) + end +end) + + +client:run("Bot BOT_TOKEN") -- replace BOT_TOKEN with your bot token diff --git a/deps/discordia/examples/pingPong.lua b/deps/discordia/examples/pingPong.lua new file mode 100644 index 0000000..e78e022 --- /dev/null +++ b/deps/discordia/examples/pingPong.lua @@ -0,0 +1,20 @@ +local discordia = require("discordia") +local client = discordia.Client() + +client:on("ready", function() -- bot is ready + print("Logged in as " .. client.user.username) +end) + +client:on("messageCreate", function(message) + + local content = message.content + + if content == "!ping" then + message:reply("Pong!") + elseif content == "!pong" then + message:reply("Ping!") + end + +end) + +client:run("Bot BOT_TOKEN") -- replace BOT_TOKEN with your bot token diff --git a/deps/discordia/init.lua b/deps/discordia/init.lua new file mode 100644 index 0000000..b10704a --- /dev/null +++ b/deps/discordia/init.lua @@ -0,0 +1,18 @@ +return { + class = require('class'), + enums = require('enums'), + extensions = require('extensions'), + package = require('./package.lua'), + Client = require('client/Client'), + Clock = require('utils/Clock'), + Color = require('utils/Color'), + Date = require('utils/Date'), + Deque = require('utils/Deque'), + Emitter = require('utils/Emitter'), + Logger = require('utils/Logger'), + Mutex = require('utils/Mutex'), + Permissions = require('utils/Permissions'), + Stopwatch = require('utils/Stopwatch'), + Time = require('utils/Time'), + storage = {}, +} diff --git a/deps/discordia/libs/class.lua b/deps/discordia/libs/class.lua new file mode 100644 index 0000000..6c83d43 --- /dev/null +++ b/deps/discordia/libs/class.lua @@ -0,0 +1,165 @@ +local format = string.format + +local meta = {} +local names = {} +local classes = {} +local objects = setmetatable({}, {__mode = 'k'}) + +function meta:__call(...) + local obj = setmetatable({}, self) + objects[obj] = true + obj:__init(...) + return obj +end + +function meta:__tostring() + return 'class ' .. self.__name +end + +local default = {} + +function default:__tostring() + return self.__name +end + +function default:__hash() + return self +end + +local function isClass(cls) + return classes[cls] +end + +local function isObject(obj) + return objects[obj] +end + +local function isSubclass(sub, cls) + if isClass(sub) and isClass(cls) then + if sub == cls then + return true + else + for _, base in ipairs(sub.__bases) do + if isSubclass(base, cls) then + return true + end + end + end + end + return false +end + +local function isInstance(obj, cls) + return isObject(obj) and isSubclass(obj.__class, cls) +end + +local function profile() + local ret = setmetatable({}, {__index = function() return 0 end}) + for obj in pairs(objects) do + local name = obj.__name + ret[name] = ret[name] + 1 + end + return ret +end + +local types = {['string'] = true, ['number'] = true, ['boolean'] = true} + +local function _getPrimitive(v) + return types[type(v)] and v or v ~= nil and tostring(v) or nil +end + +local function serialize(obj) + if isObject(obj) then + local ret = {} + for k, v in pairs(obj.__getters) do + ret[k] = _getPrimitive(v(obj)) + end + return ret + else + return _getPrimitive(obj) + end +end + +local rawtype = type +local function type(obj) + return isObject(obj) and obj.__name or rawtype(obj) +end + +return setmetatable({ + + classes = names, + isClass = isClass, + isObject = isObject, + isSubclass = isSubclass, + isInstance = isInstance, + type = type, + profile = profile, + serialize = serialize, + +}, {__call = function(_, name, ...) + + if names[name] then return error(format('Class %q already defined', name)) end + + local class = setmetatable({}, meta) + classes[class] = true + + for k, v in pairs(default) do + class[k] = v + end + + local bases = {...} + local getters = {} + local setters = {} + + for _, base in ipairs(bases) do + for k1, v1 in pairs(base) do + class[k1] = v1 + for k2, v2 in pairs(base.__getters) do + getters[k2] = v2 + end + for k2, v2 in pairs(base.__setters) do + setters[k2] = v2 + end + end + end + + class.__name = name + class.__class = class + class.__bases = bases + class.__getters = getters + class.__setters = setters + + local pool = {} + local n = #pool + + function class:__index(k) + if getters[k] then + return getters[k](self) + elseif pool[k] then + return rawget(self, pool[k]) + else + return class[k] + end + end + + function class:__newindex(k, v) + if setters[k] then + return setters[k](self, v) + elseif class[k] or getters[k] then + return error(format('Cannot overwrite protected property: %s.%s', name, k)) + elseif k:find('_', 1, true) ~= 1 then + return error(format('Cannot write property to object without leading underscore: %s.%s', name, k)) + else + if not pool[k] then + n = n + 1 + pool[k] = n + end + return rawset(self, pool[k], v) + end + end + + names[name] = class + + return class, getters, setters + +end}) diff --git a/deps/discordia/libs/client/API.lua b/deps/discordia/libs/client/API.lua new file mode 100644 index 0000000..f9228d4 --- /dev/null +++ b/deps/discordia/libs/client/API.lua @@ -0,0 +1,711 @@ +local json = require('json') +local timer = require('timer') +local http = require('coro-http') +local package = require('../../package.lua') +local Mutex = require('utils/Mutex') +local endpoints = require('endpoints') + +local request = http.request +local f, gsub, byte = string.format, string.gsub, string.byte +local max, random = math.max, math.random +local encode, decode, null = json.encode, json.decode, json.null +local insert, concat = table.insert, table.concat +local sleep = timer.sleep +local running = coroutine.running + +local BASE_URL = "https://discord.com/api/v7" + +local JSON = 'application/json' +local PRECISION = 'millisecond' +local MULTIPART = 'multipart/form-data;boundary=' +local USER_AGENT = f('DiscordBot (%s, %s)', package.homepage, package.version) + +local majorRoutes = {guilds = true, channels = true, webhooks = true} +local payloadRequired = {PUT = true, PATCH = true, POST = true} + +local function parseErrors(ret, errors, key) + for k, v in pairs(errors) do + if k == '_errors' then + for _, err in ipairs(v) do + insert(ret, f('%s in %s : %s', err.code, key or 'payload', err.message)) + end + else + if key then + parseErrors(ret, v, f(k:find("^[%a_][%a%d_]*$") and '%s.%s' or tonumber(k) and '%s[%d]' or '%s[%q]', key, k)) + else + parseErrors(ret, v, k) + end + end + end + return concat(ret, '\n\t') +end + +local function sub(path) + return not majorRoutes[path] and path .. '/:id' +end + +local function route(method, endpoint) + + -- special case for reactions + if endpoint:find('reactions') then + endpoint = endpoint:match('.*/reactions') + end + + -- remove the ID from minor routes + endpoint = endpoint:gsub('(%a+)/%d+', sub) + + -- special case for message deletions + if method == 'DELETE' then + local i, j = endpoint:find('/channels/%d+/messages') + if i == 1 and j == #endpoint then + endpoint = method .. endpoint + end + end + + return endpoint + +end + +local function generateBoundary(files, boundary) + boundary = boundary or tostring(random(0, 9)) + for _, v in ipairs(files) do + if v[2]:find(boundary, 1, true) then + return generateBoundary(files, boundary .. random(0, 9)) + end + end + return boundary +end + +local function attachFiles(payload, files) + local boundary = generateBoundary(files) + local ret = { + '--' .. boundary, + 'Content-Disposition:form-data;name="payload_json"', + 'Content-Type:application/json\r\n', + payload, + } + for i, v in ipairs(files) do + insert(ret, '--' .. boundary) + insert(ret, f('Content-Disposition:form-data;name="file%i";filename=%q', i, v[1])) + insert(ret, 'Content-Type:application/octet-stream\r\n') + insert(ret, v[2]) + end + insert(ret, '--' .. boundary .. '--') + return concat(ret, '\r\n'), boundary +end + +local mutexMeta = { + __mode = 'v', + __index = function(self, k) + self[k] = Mutex() + return self[k] + end +} + +local function tohex(char) + return f('%%%02X', byte(char)) +end + +local function urlencode(obj) + return (gsub(tostring(obj), '%W', tohex)) +end + +local API = require('class')('API') + +function API:__init(client) + self._client = client + self._mutexes = setmetatable({}, mutexMeta) +end + +function API:authenticate(token) + self._token = token + return self:getCurrentUser() +end + +function API:request(method, endpoint, payload, query, files) + + local _, main = running() + if main then + return error('Cannot make HTTP request outside of a coroutine', 2) + end + + local url = BASE_URL .. endpoint + + if query and next(query) then + url = {url} + for k, v in pairs(query) do + insert(url, #url == 1 and '?' or '&') + insert(url, urlencode(k)) + insert(url, '=') + insert(url, urlencode(v)) + end + url = concat(url) + end + + local req = { + {'User-Agent', USER_AGENT}, + {'X-RateLimit-Precision', PRECISION}, + {'Authorization', self._token}, + } + + if payloadRequired[method] then + payload = payload and encode(payload) or '{}' + if files and next(files) then + local boundary + payload, boundary = attachFiles(payload, files) + insert(req, {'Content-Type', MULTIPART .. boundary}) + else + insert(req, {'Content-Type', JSON}) + end + insert(req, {'Content-Length', #payload}) + end + + local mutex = self._mutexes[route(method, endpoint)] + + mutex:lock() + local data, err, delay = self:commit(method, url, req, payload, 0) + mutex:unlockAfter(delay) + + if data then + return data + else + return nil, err + end + +end + +function API:commit(method, url, req, payload, retries) + + local client = self._client + local options = client._options + local delay = options.routeDelay + + local success, res, msg = pcall(request, method, url, req, payload) + + if not success then + return nil, res, delay + end + + for i, v in ipairs(res) do + res[v[1]:lower()] = v[2] + res[i] = nil + end + + if res['x-ratelimit-remaining'] == '0' then + delay = max(1000 * res['x-ratelimit-reset-after'], delay) + end + + local data = res['content-type'] == JSON and decode(msg, 1, null) or msg + + if res.code < 300 then + + client:debug('%i - %s : %s %s', res.code, res.reason, method, url) + return data, nil, delay + + else + + if type(data) == 'table' then + + local retry + if res.code == 429 then -- TODO: global ratelimiting + delay = data.retry_after + retry = retries < options.maxRetries + elseif res.code == 502 then + delay = delay + random(2000) + retry = retries < options.maxRetries + end + + if retry then + client:warning('%i - %s : retrying after %i ms : %s %s', res.code, res.reason, delay, method, url) + sleep(delay) + return self:commit(method, url, req, payload, retries + 1) + end + + if data.code and data.message then + msg = f('HTTP Error %i : %s', data.code, data.message) + else + msg = 'HTTP Error' + end + if data.errors then + msg = parseErrors({msg}, data.errors) + end + + end + + client:error('%i - %s : %s %s', res.code, res.reason, method, url) + return nil, msg, delay + + end + +end + +-- start of auto-generated methods -- + +function API:getGuildAuditLog(guild_id, query) + local endpoint = f(endpoints.GUILD_AUDIT_LOGS, guild_id) + return self:request("GET", endpoint, nil, query) +end + +function API:getChannel(channel_id) -- not exposed, use cache + local endpoint = f(endpoints.CHANNEL, channel_id) + return self:request("GET", endpoint) +end + +function API:modifyChannel(channel_id, payload) -- Channel:_modify + local endpoint = f(endpoints.CHANNEL, channel_id) + return self:request("PATCH", endpoint, payload) +end + +function API:deleteChannel(channel_id) -- Channel:delete + local endpoint = f(endpoints.CHANNEL, channel_id) + return self:request("DELETE", endpoint) +end + +function API:getChannelMessages(channel_id, query) -- TextChannel:get[First|Last]Message, TextChannel:getMessages + local endpoint = f(endpoints.CHANNEL_MESSAGES, channel_id) + return self:request("GET", endpoint, nil, query) +end + +function API:getChannelMessage(channel_id, message_id) -- TextChannel:getMessage fallback + local endpoint = f(endpoints.CHANNEL_MESSAGE, channel_id, message_id) + return self:request("GET", endpoint) +end + +function API:createMessage(channel_id, payload, files) -- TextChannel:send + local endpoint = f(endpoints.CHANNEL_MESSAGES, channel_id) + return self:request("POST", endpoint, payload, nil, files) +end + +function API:createReaction(channel_id, message_id, emoji, payload) -- Message:addReaction + local endpoint = f(endpoints.CHANNEL_MESSAGE_REACTION_ME, channel_id, message_id, urlencode(emoji)) + return self:request("PUT", endpoint, payload) +end + +function API:deleteOwnReaction(channel_id, message_id, emoji) -- Message:removeReaction + local endpoint = f(endpoints.CHANNEL_MESSAGE_REACTION_ME, channel_id, message_id, urlencode(emoji)) + return self:request("DELETE", endpoint) +end + +function API:deleteUserReaction(channel_id, message_id, emoji, user_id) -- Message:removeReaction + local endpoint = f(endpoints.CHANNEL_MESSAGE_REACTION_USER, channel_id, message_id, urlencode(emoji), user_id) + return self:request("DELETE", endpoint) +end + +function API:getReactions(channel_id, message_id, emoji, query) -- Reaction:getUsers + local endpoint = f(endpoints.CHANNEL_MESSAGE_REACTION, channel_id, message_id, urlencode(emoji)) + return self:request("GET", endpoint, nil, query) +end + +function API:deleteAllReactions(channel_id, message_id) -- Message:clearReactions + local endpoint = f(endpoints.CHANNEL_MESSAGE_REACTIONS, channel_id, message_id) + return self:request("DELETE", endpoint) +end + +function API:editMessage(channel_id, message_id, payload) -- Message:_modify + local endpoint = f(endpoints.CHANNEL_MESSAGE, channel_id, message_id) + return self:request("PATCH", endpoint, payload) +end + +function API:deleteMessage(channel_id, message_id) -- Message:delete + local endpoint = f(endpoints.CHANNEL_MESSAGE, channel_id, message_id) + return self:request("DELETE", endpoint) +end + +function API:bulkDeleteMessages(channel_id, payload) -- GuildTextChannel:bulkDelete + local endpoint = f(endpoints.CHANNEL_MESSAGES_BULK_DELETE, channel_id) + return self:request("POST", endpoint, payload) +end + +function API:editChannelPermissions(channel_id, overwrite_id, payload) -- various PermissionOverwrite methods + local endpoint = f(endpoints.CHANNEL_PERMISSION, channel_id, overwrite_id) + return self:request("PUT", endpoint, payload) +end + +function API:getChannelInvites(channel_id) -- GuildChannel:getInvites + local endpoint = f(endpoints.CHANNEL_INVITES, channel_id) + return self:request("GET", endpoint) +end + +function API:createChannelInvite(channel_id, payload) -- GuildChannel:createInvite + local endpoint = f(endpoints.CHANNEL_INVITES, channel_id) + return self:request("POST", endpoint, payload) +end + +function API:deleteChannelPermission(channel_id, overwrite_id) -- PermissionOverwrite:delete + local endpoint = f(endpoints.CHANNEL_PERMISSION, channel_id, overwrite_id) + return self:request("DELETE", endpoint) +end + +function API:triggerTypingIndicator(channel_id, payload) -- TextChannel:broadcastTyping + local endpoint = f(endpoints.CHANNEL_TYPING, channel_id) + return self:request("POST", endpoint, payload) +end + +function API:getPinnedMessages(channel_id) -- TextChannel:getPinnedMessages + local endpoint = f(endpoints.CHANNEL_PINS, channel_id) + return self:request("GET", endpoint) +end + +function API:addPinnedChannelMessage(channel_id, message_id, payload) -- Message:pin + local endpoint = f(endpoints.CHANNEL_PIN, channel_id, message_id) + return self:request("PUT", endpoint, payload) +end + +function API:deletePinnedChannelMessage(channel_id, message_id) -- Message:unpin + local endpoint = f(endpoints.CHANNEL_PIN, channel_id, message_id) + return self:request("DELETE", endpoint) +end + +function API:groupDMAddRecipient(channel_id, user_id, payload) -- GroupChannel:addRecipient + local endpoint = f(endpoints.CHANNEL_RECIPIENT, channel_id, user_id) + return self:request("PUT", endpoint, payload) +end + +function API:groupDMRemoveRecipient(channel_id, user_id) -- GroupChannel:removeRecipient + local endpoint = f(endpoints.CHANNEL_RECIPIENT, channel_id, user_id) + return self:request("DELETE", endpoint) +end + +function API:listGuildEmojis(guild_id) -- not exposed, use cache + local endpoint = f(endpoints.GUILD_EMOJIS, guild_id) + return self:request("GET", endpoint) +end + +function API:getGuildEmoji(guild_id, emoji_id) -- not exposed, use cache + local endpoint = f(endpoints.GUILD_EMOJI, guild_id, emoji_id) + return self:request("GET", endpoint) +end + +function API:createGuildEmoji(guild_id, payload) -- Guild:createEmoji + local endpoint = f(endpoints.GUILD_EMOJIS, guild_id) + return self:request("POST", endpoint, payload) +end + +function API:modifyGuildEmoji(guild_id, emoji_id, payload) -- Emoji:_modify + local endpoint = f(endpoints.GUILD_EMOJI, guild_id, emoji_id) + return self:request("PATCH", endpoint, payload) +end + +function API:deleteGuildEmoji(guild_id, emoji_id) -- Emoji:delete + local endpoint = f(endpoints.GUILD_EMOJI, guild_id, emoji_id) + return self:request("DELETE", endpoint) +end + +function API:createGuild(payload) -- Client:createGuild + local endpoint = endpoints.GUILDS + return self:request("POST", endpoint, payload) +end + +function API:getGuild(guild_id) -- not exposed, use cache + local endpoint = f(endpoints.GUILD, guild_id) + return self:request("GET", endpoint) +end + +function API:modifyGuild(guild_id, payload) -- Guild:_modify + local endpoint = f(endpoints.GUILD, guild_id) + return self:request("PATCH", endpoint, payload) +end + +function API:deleteGuild(guild_id) -- Guild:delete + local endpoint = f(endpoints.GUILD, guild_id) + return self:request("DELETE", endpoint) +end + +function API:getGuildChannels(guild_id) -- not exposed, use cache + local endpoint = f(endpoints.GUILD_CHANNELS, guild_id) + return self:request("GET", endpoint) +end + +function API:createGuildChannel(guild_id, payload) -- Guild:create[Text|Voice]Channel + local endpoint = f(endpoints.GUILD_CHANNELS, guild_id) + return self:request("POST", endpoint, payload) +end + +function API:modifyGuildChannelPositions(guild_id, payload) -- GuildChannel:move[Up|Down] + local endpoint = f(endpoints.GUILD_CHANNELS, guild_id) + return self:request("PATCH", endpoint, payload) +end + +function API:getGuildMember(guild_id, user_id) -- Guild:getMember fallback + local endpoint = f(endpoints.GUILD_MEMBER, guild_id, user_id) + return self:request("GET", endpoint) +end + +function API:listGuildMembers(guild_id) -- not exposed, use cache + local endpoint = f(endpoints.GUILD_MEMBERS, guild_id) + return self:request("GET", endpoint) +end + +function API:addGuildMember(guild_id, user_id, payload) -- not exposed, limited use + local endpoint = f(endpoints.GUILD_MEMBER, guild_id, user_id) + return self:request("PUT", endpoint, payload) +end + +function API:modifyGuildMember(guild_id, user_id, payload) -- various Member methods + local endpoint = f(endpoints.GUILD_MEMBER, guild_id, user_id) + return self:request("PATCH", endpoint, payload) +end + +function API:modifyCurrentUsersNick(guild_id, payload) -- Member:setNickname + local endpoint = f(endpoints.GUILD_MEMBER_ME_NICK, guild_id) + return self:request("PATCH", endpoint, payload) +end + +function API:addGuildMemberRole(guild_id, user_id, role_id, payload) -- Member:addrole + local endpoint = f(endpoints.GUILD_MEMBER_ROLE, guild_id, user_id, role_id) + return self:request("PUT", endpoint, payload) +end + +function API:removeGuildMemberRole(guild_id, user_id, role_id) -- Member:removeRole + local endpoint = f(endpoints.GUILD_MEMBER_ROLE, guild_id, user_id, role_id) + return self:request("DELETE", endpoint) +end + +function API:removeGuildMember(guild_id, user_id, query) -- Guild:kickUser + local endpoint = f(endpoints.GUILD_MEMBER, guild_id, user_id) + return self:request("DELETE", endpoint, nil, query) +end + +function API:getGuildBans(guild_id) -- Guild:getBans + local endpoint = f(endpoints.GUILD_BANS, guild_id) + return self:request("GET", endpoint) +end + +function API:getGuildBan(guild_id, user_id) -- Guild:getBan + local endpoint = f(endpoints.GUILD_BAN, guild_id, user_id) + return self:request("GET", endpoint) +end + +function API:createGuildBan(guild_id, user_id, query) -- Guild:banUser + local endpoint = f(endpoints.GUILD_BAN, guild_id, user_id) + return self:request("PUT", endpoint, nil, query) +end + +function API:removeGuildBan(guild_id, user_id, query) -- Guild:unbanUser / Ban:delete + local endpoint = f(endpoints.GUILD_BAN, guild_id, user_id) + return self:request("DELETE", endpoint, nil, query) +end + +function API:getGuildRoles(guild_id) -- not exposed, use cache + local endpoint = f(endpoints.GUILD_ROLES, guild_id) + return self:request("GET", endpoint) +end + +function API:createGuildRole(guild_id, payload) -- Guild:createRole + local endpoint = f(endpoints.GUILD_ROLES, guild_id) + return self:request("POST", endpoint, payload) +end + +function API:modifyGuildRolePositions(guild_id, payload) -- Role:move[Up|Down] + local endpoint = f(endpoints.GUILD_ROLES, guild_id) + return self:request("PATCH", endpoint, payload) +end + +function API:modifyGuildRole(guild_id, role_id, payload) -- Role:_modify + local endpoint = f(endpoints.GUILD_ROLE, guild_id, role_id) + return self:request("PATCH", endpoint, payload) +end + +function API:deleteGuildRole(guild_id, role_id) -- Role:delete + local endpoint = f(endpoints.GUILD_ROLE, guild_id, role_id) + return self:request("DELETE", endpoint) +end + +function API:getGuildPruneCount(guild_id, query) -- Guild:getPruneCount + local endpoint = f(endpoints.GUILD_PRUNE, guild_id) + return self:request("GET", endpoint, nil, query) +end + +function API:beginGuildPrune(guild_id, payload, query) -- Guild:pruneMembers + local endpoint = f(endpoints.GUILD_PRUNE, guild_id) + return self:request("POST", endpoint, payload, query) +end + +function API:getGuildVoiceRegions(guild_id) -- Guild:listVoiceRegions + local endpoint = f(endpoints.GUILD_REGIONS, guild_id) + return self:request("GET", endpoint) +end + +function API:getGuildInvites(guild_id) -- Guild:getInvites + local endpoint = f(endpoints.GUILD_INVITES, guild_id) + return self:request("GET", endpoint) +end + +function API:getGuildIntegrations(guild_id) -- not exposed, maybe in the future + local endpoint = f(endpoints.GUILD_INTEGRATIONS, guild_id) + return self:request("GET", endpoint) +end + +function API:createGuildIntegration(guild_id, payload) -- not exposed, maybe in the future + local endpoint = f(endpoints.GUILD_INTEGRATIONS, guild_id) + return self:request("POST", endpoint, payload) +end + +function API:modifyGuildIntegration(guild_id, integration_id, payload) -- not exposed, maybe in the future + local endpoint = f(endpoints.GUILD_INTEGRATION, guild_id, integration_id) + return self:request("PATCH", endpoint, payload) +end + +function API:deleteGuildIntegration(guild_id, integration_id) -- not exposed, maybe in the future + local endpoint = f(endpoints.GUILD_INTEGRATION, guild_id, integration_id) + return self:request("DELETE", endpoint) +end + +function API:syncGuildIntegration(guild_id, integration_id, payload) -- not exposed, maybe in the future + local endpoint = f(endpoints.GUILD_INTEGRATION_SYNC, guild_id, integration_id) + return self:request("POST", endpoint, payload) +end + +function API:getGuildEmbed(guild_id) -- not exposed, maybe in the future + local endpoint = f(endpoints.GUILD_EMBED, guild_id) + return self:request("GET", endpoint) +end + +function API:modifyGuildEmbed(guild_id, payload) -- not exposed, maybe in the future + local endpoint = f(endpoints.GUILD_EMBED, guild_id) + return self:request("PATCH", endpoint, payload) +end + +function API:getInvite(invite_code, query) -- Client:getInvite + local endpoint = f(endpoints.INVITE, invite_code) + return self:request("GET", endpoint, nil, query) +end + +function API:deleteInvite(invite_code) -- Invite:delete + local endpoint = f(endpoints.INVITE, invite_code) + return self:request("DELETE", endpoint) +end + +function API:acceptInvite(invite_code, payload) -- not exposed, invalidates tokens + local endpoint = f(endpoints.INVITE, invite_code) + return self:request("POST", endpoint, payload) +end + +function API:getCurrentUser() -- API:authenticate + local endpoint = endpoints.USER_ME + return self:request("GET", endpoint) +end + +function API:getUser(user_id) -- Client:getUser + local endpoint = f(endpoints.USER, user_id) + return self:request("GET", endpoint) +end + +function API:modifyCurrentUser(payload) -- Client:_modify + local endpoint = endpoints.USER_ME + return self:request("PATCH", endpoint, payload) +end + +function API:getCurrentUserGuilds() -- not exposed, use cache + local endpoint = endpoints.USER_ME_GUILDS + return self:request("GET", endpoint) +end + +function API:leaveGuild(guild_id) -- Guild:leave + local endpoint = f(endpoints.USER_ME_GUILD, guild_id) + return self:request("DELETE", endpoint) +end + +function API:getUserDMs() -- not exposed, use cache + local endpoint = endpoints.USER_ME_CHANNELS + return self:request("GET", endpoint) +end + +function API:createDM(payload) -- User:getPrivateChannel fallback + local endpoint = endpoints.USER_ME_CHANNELS + return self:request("POST", endpoint, payload) +end + +function API:createGroupDM(payload) -- Client:createGroupChannel + local endpoint = endpoints.USER_ME_CHANNELS + return self:request("POST", endpoint, payload) +end + +function API:getUsersConnections() -- Client:getConnections + local endpoint = endpoints.USER_ME_CONNECTIONS + return self:request("GET", endpoint) +end + +function API:listVoiceRegions() -- Client:listVoiceRegions + local endpoint = endpoints.VOICE_REGIONS + return self:request("GET", endpoint) +end + +function API:createWebhook(channel_id, payload) -- GuildTextChannel:createWebhook + local endpoint = f(endpoints.CHANNEL_WEBHOOKS, channel_id) + return self:request("POST", endpoint, payload) +end + +function API:getChannelWebhooks(channel_id) -- GuildTextChannel:getWebhooks + local endpoint = f(endpoints.CHANNEL_WEBHOOKS, channel_id) + return self:request("GET", endpoint) +end + +function API:getGuildWebhooks(guild_id) -- Guild:getWebhooks + local endpoint = f(endpoints.GUILD_WEBHOOKS, guild_id) + return self:request("GET", endpoint) +end + +function API:getWebhook(webhook_id) -- Client:getWebhook + local endpoint = f(endpoints.WEBHOOK, webhook_id) + return self:request("GET", endpoint) +end + +function API:getWebhookWithToken(webhook_id, webhook_token) -- not exposed, needs webhook client + local endpoint = f(endpoints.WEBHOOK_TOKEN, webhook_id, webhook_token) + return self:request("GET", endpoint) +end + +function API:modifyWebhook(webhook_id, payload) -- Webhook:_modify + local endpoint = f(endpoints.WEBHOOK, webhook_id) + return self:request("PATCH", endpoint, payload) +end + +function API:modifyWebhookWithToken(webhook_id, webhook_token, payload) -- not exposed, needs webhook client + local endpoint = f(endpoints.WEBHOOK_TOKEN, webhook_id, webhook_token) + return self:request("PATCH", endpoint, payload) +end + +function API:deleteWebhook(webhook_id) -- Webhook:delete + local endpoint = f(endpoints.WEBHOOK, webhook_id) + return self:request("DELETE", endpoint) +end + +function API:deleteWebhookWithToken(webhook_id, webhook_token) -- not exposed, needs webhook client + local endpoint = f(endpoints.WEBHOOK_TOKEN, webhook_id, webhook_token) + return self:request("DELETE", endpoint) +end + +function API:executeWebhook(webhook_id, webhook_token, payload) -- not exposed, needs webhook client + local endpoint = f(endpoints.WEBHOOK_TOKEN, webhook_id, webhook_token) + return self:request("POST", endpoint, payload) +end + +function API:executeSlackCompatibleWebhook(webhook_id, webhook_token, payload) -- not exposed, needs webhook client + local endpoint = f(endpoints.WEBHOOK_TOKEN_SLACK, webhook_id, webhook_token) + return self:request("POST", endpoint, payload) +end + +function API:executeGitHubCompatibleWebhook(webhook_id, webhook_token, payload) -- not exposed, needs webhook client + local endpoint = f(endpoints.WEBHOOK_TOKEN_GITHUB, webhook_id, webhook_token) + return self:request("POST", endpoint, payload) +end + +function API:getGateway() -- Client:run + local endpoint = endpoints.GATEWAY + return self:request("GET", endpoint) +end + +function API:getGatewayBot() -- Client:run + local endpoint = endpoints.GATEWAY_BOT + return self:request("GET", endpoint) +end + +function API:getCurrentApplicationInformation() -- Client:run + local endpoint = endpoints.OAUTH2_APPLICATION_ME + return self:request("GET", endpoint) +end + +-- end of auto-generated methods -- + +return API diff --git a/deps/discordia/libs/client/Client.lua b/deps/discordia/libs/client/Client.lua new file mode 100644 index 0000000..8d624f1 --- /dev/null +++ b/deps/discordia/libs/client/Client.lua @@ -0,0 +1,679 @@ +--[=[ +@c Client x Emitter +@t ui +@op options table +@d The main point of entry into a Discordia application. All data relevant to +Discord is accessible through a client instance or its child objects after a +connection to Discord is established with the `run` method. In other words, +client data should not be expected and most client methods should not be called +until after the `ready` event is received. Base emitter methods may be called +at any time. See [[client options]]. +]=] + +local fs = require('fs') +local json = require('json') + +local constants = require('constants') +local enums = require('enums') +local package = require('../../package.lua') + +local API = require('client/API') +local Shard = require('client/Shard') +local Resolver = require('client/Resolver') + +local GroupChannel = require('containers/GroupChannel') +local Guild = require('containers/Guild') +local PrivateChannel = require('containers/PrivateChannel') +local User = require('containers/User') +local Invite = require('containers/Invite') +local Webhook = require('containers/Webhook') +local Relationship = require('containers/Relationship') + +local Cache = require('iterables/Cache') +local WeakCache = require('iterables/WeakCache') +local Emitter = require('utils/Emitter') +local Logger = require('utils/Logger') +local Mutex = require('utils/Mutex') + +local VoiceManager = require('voice/VoiceManager') + +local encode, decode, null = json.encode, json.decode, json.null +local readFileSync, writeFileSync = fs.readFileSync, fs.writeFileSync + +local logLevel = enums.logLevel +local gameType = enums.gameType + +local wrap = coroutine.wrap +local time, difftime = os.time, os.difftime +local format = string.format + +local CACHE_AGE = constants.CACHE_AGE +local GATEWAY_VERSION = constants.GATEWAY_VERSION + +-- do not change these options here +-- pass a custom table on client initialization instead +local defaultOptions = { + routeDelay = 250, + maxRetries = 5, + shardCount = 0, + firstShard = 0, + lastShard = -1, + largeThreshold = 100, + cacheAllMembers = false, + autoReconnect = true, + compress = true, + bitrate = 64000, + logFile = 'discordia.log', + logLevel = logLevel.info, + gatewayFile = 'gateway.json', + dateTime = '%F %T', + syncGuilds = false, +} + +local function parseOptions(customOptions) + if type(customOptions) == 'table' then + local options = {} + for k, default in pairs(defaultOptions) do -- load options + local custom = customOptions[k] + if custom ~= nil then + options[k] = custom + else + options[k] = default + end + end + for k, v in pairs(customOptions) do -- validate options + local default = type(defaultOptions[k]) + local custom = type(v) + if default ~= custom then + return error(format('invalid client option %q (%s expected, got %s)', k, default, custom), 3) + end + if custom == 'number' and (v < 0 or v % 1 ~= 0) then + return error(format('invalid client option %q (number must be a positive integer)', k), 3) + end + end + return options + else + return defaultOptions + end +end + +local Client, get = require('class')('Client', Emitter) + +function Client:__init(options) + Emitter.__init(self) + options = parseOptions(options) + self._options = options + self._shards = {} + self._api = API(self) + self._mutex = Mutex() + self._users = Cache({}, User, self) + self._guilds = Cache({}, Guild, self) + self._group_channels = Cache({}, GroupChannel, self) + self._private_channels = Cache({}, PrivateChannel, self) + self._relationships = Cache({}, Relationship, self) + self._webhooks = WeakCache({}, Webhook, self) -- used for audit logs + self._logger = Logger(options.logLevel, options.dateTime, options.logFile) + self._voice = VoiceManager(self) + self._role_map = {} + self._emoji_map = {} + self._channel_map = {} + self._events = require('client/EventHandler') +end + +for name, level in pairs(logLevel) do + Client[name] = function(self, fmt, ...) + local msg = self._logger:log(level, fmt, ...) + return self:emit(name, msg or format(fmt, ...)) + end +end + +function Client:_deprecated(clsName, before, after) + local info = debug.getinfo(3) + return self:warning( + '%s:%s: %s.%s is deprecated; use %s.%s instead', + info.short_src, + info.currentline, + clsName, + before, + clsName, + after + ) +end + +local function run(self, token) + + self:info('Discordia %s', package.version) + self:info('Connecting to Discord...') + + local api = self._api + local users = self._users + local options = self._options + + local user, err1 = api:authenticate(token) + if not user then + return self:error('Could not authenticate, check token: ' .. err1) + end + self._user = users:_insert(user) + self._token = token + + self:info('Authenticated as %s#%s', user.username, user.discriminator) + + local now = time() + local url, count, owner + + local cache = readFileSync(options.gatewayFile) + cache = cache and decode(cache) + + if cache then + local d = cache[user.id] + if d and difftime(now, d.timestamp) < CACHE_AGE then + url = cache.url + if user.bot then + count = d.shards + owner = d.owner + else + count = 1 + owner = user + end + end + else + cache = {} + end + + if not url or not owner then + + if user.bot then + + local gateway, err2 = api:getGatewayBot() + if not gateway then + return self:error('Could not get gateway: ' .. err2) + end + + local app, err3 = api:getCurrentApplicationInformation() + if not app then + return self:error('Could not get application information: ' .. err3) + end + + url = gateway.url + count = gateway.shards + owner = app.owner + + cache[user.id] = {owner = owner, shards = count, timestamp = now} + + else + + local gateway, err2 = api:getGateway() + if not gateway then + return self:error('Could not get gateway: ' .. err2) + end + + url = gateway.url + count = 1 + owner = user + + cache[user.id] = {timestamp = now} + + end + + cache.url = url + + writeFileSync(options.gatewayFile, encode(cache)) + + end + + self._owner = users:_insert(owner) + + if options.shardCount > 0 then + if count ~= options.shardCount then + self:warning('Requested shard count (%i) is different from recommended count (%i)', options.shardCount, count) + end + count = options.shardCount + end + + local first, last = options.firstShard, options.lastShard + + if last < 0 then + last = count - 1 + end + + if last < first then + return self:error('First shard ID (%i) is greater than last shard ID (%i)', first, last) + end + + local d = last - first + 1 + if d > count then + return self:error('Shard count (%i) is less than target shard range (%i)', count, d) + end + + if first == last then + self:info('Launching shard %i (%i out of %i)...', first, d, count) + else + self:info('Launching shards %i through %i (%i out of %i)...', first, last, d, count) + end + + self._total_shard_count = count + self._shard_count = d + + for id = first, last do + self._shards[id] = Shard(id, self) + end + + local path = format('/?v=%i&encoding=json', GATEWAY_VERSION) + for _, shard in pairs(self._shards) do + wrap(shard.connect)(shard, url, path) + shard:identifyWait() + end + +end + +--[=[ +@m run +@p token string +@op presence table +@r nil +@d Authenticates the current user via HTTPS and launches as many WSS gateway +shards as are required or requested. By using coroutines that are automatically +managed by Luvit libraries and a libuv event loop, multiple clients per process +and multiple shards per client can operate concurrently. This should be the last +method called after all other code and event handlers have been initialized. If +a presence table is provided, it will act as if the user called `setStatus` +and `setGame` after `run`. +]=] +function Client:run(token, presence) + self._presence = presence or {} + return wrap(run)(self, token) +end + +--[=[ +@m stop +@t ws +@r nil +@d Disconnects all shards and effectively stop their loops. This does not +empty any data that the client may have cached. +]=] +function Client:stop() + for _, shard in pairs(self._shards) do + shard:disconnect() + end +end + +function Client:_modify(payload) + local data, err = self._api:modifyCurrentUser(payload) + if data then + data.token = nil + self._user:_load(data) + return true + else + return false, err + end +end + +--[=[ +@m setUsername +@t http +@p username string +@r boolean +@d Sets the client's username. This must be between 2 and 32 characters in +length. This does not change the application name. +]=] +function Client:setUsername(username) + return self:_modify({username = username or null}) +end + +--[=[ +@m setAvatar +@t http +@p avatar Base64-Resolveable +@r boolean +@d Sets the client's avatar. To remove the avatar, pass an empty string or nil. +This does not change the application image. +]=] +function Client:setAvatar(avatar) + avatar = avatar and Resolver.base64(avatar) + return self:_modify({avatar = avatar or null}) +end + +--[=[ +@m createGuild +@t http +@p name string +@r boolean +@d Creates a new guild. The name must be between 2 and 100 characters in length. +This method may not work if the current user is in too many guilds. Note that +this does not return the created guild object; wait for the corresponding +`guildCreate` event if you need the object. +]=] +function Client:createGuild(name) + local data, err = self._api:createGuild({name = name}) + if data then + return true + else + return false, err + end +end + +--[=[ +@m createGroupChannel +@t http +@r GroupChannel +@d Creates a new group channel. This method is only available for user accounts. +]=] +function Client:createGroupChannel() + local data, err = self._api:createGroupDM() + if data then + return self._group_channels:_insert(data) + else + return nil, err + end +end + +--[=[ +@m getWebhook +@t http +@p id string +@r Webhook +@d Gets a webhook object by ID. This always makes an HTTP request to obtain a +static object that is not cached and is not updated by gateway events. +]=] +function Client:getWebhook(id) + local data, err = self._api:getWebhook(id) + if data then + return Webhook(data, self) + else + return nil, err + end +end + +--[=[ +@m getInvite +@t http +@p code string +@op counts boolean +@r Invite +@d Gets an invite object by code. This always makes an HTTP request to obtain a +static object that is not cached and is not updated by gateway events. +]=] +function Client:getInvite(code, counts) + local data, err = self._api:getInvite(code, counts and {with_counts = true}) + if data then + return Invite(data, self) + else + return nil, err + end +end + +--[=[ +@m getUser +@t http? +@p id User-ID-Resolvable +@r User +@d Gets a user object by ID. If the object is already cached, then the cached +object will be returned; otherwise, an HTTP request is made. Under circumstances +which should be rare, the user object may be an old version, not updated by +gateway events. +]=] +function Client:getUser(id) + id = Resolver.userId(id) + local user = self._users:get(id) + if user then + return user + else + local data, err = self._api:getUser(id) + if data then + return self._users:_insert(data) + else + return nil, err + end + end +end + +--[=[ +@m getGuild +@t mem +@p id Guild-ID-Resolvable +@r Guild +@d Gets a guild object by ID. The current user must be in the guild and the client +must be running the appropriate shard that serves this guild. This method never +makes an HTTP request to obtain a guild. +]=] +function Client:getGuild(id) + id = Resolver.guildId(id) + return self._guilds:get(id) +end + +--[=[ +@m getChannel +@t mem +@p id Channel-ID-Resolvable +@r Channel +@d Gets a channel object by ID. For guild channels, the current user must be in +the channel's guild and the client must be running the appropriate shard that +serves the channel's guild. + +For private channels, the channel must have been previously opened and cached. +If the channel is not cached, `User:getPrivateChannel` should be used instead. +]=] +function Client:getChannel(id) + id = Resolver.channelId(id) + local guild = self._channel_map[id] + if guild then + return guild._text_channels:get(id) or guild._voice_channels:get(id) or guild._categories:get(id) + else + return self._private_channels:get(id) or self._group_channels:get(id) + end +end + +--[=[ +@m getRole +@t mem +@p id Role-ID-Resolvable +@r Role +@d Gets a role object by ID. The current user must be in the role's guild and +the client must be running the appropriate shard that serves the role's guild. +]=] +function Client:getRole(id) + id = Resolver.roleId(id) + local guild = self._role_map[id] + return guild and guild._roles:get(id) +end + +--[=[ +@m getEmoji +@t mem +@p id Emoji-ID-Resolvable +@r Emoji +@d Gets an emoji object by ID. The current user must be in the emoji's guild and +the client must be running the appropriate shard that serves the emoji's guild. +]=] +function Client:getEmoji(id) + id = Resolver.emojiId(id) + local guild = self._emoji_map[id] + return guild and guild._emojis:get(id) +end + +--[=[ +@m listVoiceRegions +@t http +@r table +@d Returns a raw data table that contains a list of voice regions as provided by +Discord, with no formatting beyond what is provided by the Discord API. +]=] +function Client:listVoiceRegions() + return self._api:listVoiceRegions() +end + +--[=[ +@m getConnections +@t http +@r table +@d Returns a raw data table that contains a list of connections as provided by +Discord, with no formatting beyond what is provided by the Discord API. +This is unrelated to voice connections. +]=] +function Client:getConnections() + return self._api:getUsersConnections() +end + +--[=[ +@m getApplicationInformation +@t http +@r table +@d Returns a raw data table that contains information about the current OAuth2 +application, with no formatting beyond what is provided by the Discord API. +]=] +function Client:getApplicationInformation() + return self._api:getCurrentApplicationInformation() +end + +local function updateStatus(self) + local presence = self._presence + presence.afk = presence.afk or null + presence.game = presence.game or null + presence.since = presence.since or null + presence.status = presence.status or null + for _, shard in pairs(self._shards) do + shard:updateStatus(presence) + end +end + +--[=[ +@m setStatus +@t ws +@p status string +@r nil +@d Sets the current users's status on all shards that are managed by this client. +See the `status` enumeration for acceptable status values. +]=] +function Client:setStatus(status) + if type(status) == 'string' then + self._presence.status = status + if status == 'idle' then + self._presence.since = 1000 * time() + else + self._presence.since = null + end + else + self._presence.status = null + self._presence.since = null + end + return updateStatus(self) +end + +--[=[ +@m setGame +@t ws +@p game string/table +@r nil +@d Sets the current users's game on all shards that are managed by this client. +If a string is passed, it is treated as the game name. If a table is passed, it +must have a `name` field and may optionally have a `url` or `type` field. Pass `nil` to +remove the game status. +]=] +function Client:setGame(game) + if type(game) == 'string' then + game = {name = game, type = gameType.default} + elseif type(game) == 'table' then + if type(game.name) == 'string' then + if type(game.type) ~= 'number' then + if type(game.url) == 'string' then + game.type = gameType.streaming + else + game.type = gameType.default + end + end + else + game = null + end + else + game = null + end + self._presence.game = game + return updateStatus(self) +end + +--[=[ +@m setAFK +@t ws +@p afk boolean +@r nil +@d Set the current user's AFK status on all shards that are managed by this client. +This generally applies to user accounts and their push notifications. +]=] +function Client:setAFK(afk) + if type(afk) == 'boolean' then + self._presence.afk = afk + else + self._presence.afk = null + end + return updateStatus(self) +end + +--[=[@p shardCount number/nil The number of shards that this client is managing.]=] +function get.shardCount(self) + return self._shard_count +end + +--[=[@p totalShardCount number/nil The total number of shards that the current user is on.]=] +function get.totalShardCount(self) + return self._total_shard_count +end + +--[=[@p user User/nil User object representing the current user.]=] +function get.user(self) + return self._user +end + +--[=[@p owner User/nil User object representing the current user's owner.]=] +function get.owner(self) + return self._owner +end + +--[=[@p verified boolean/nil Whether the current user's owner's account is verified.]=] +function get.verified(self) + return self._user and self._user._verified +end + +--[=[@p mfaEnabled boolean/nil Whether the current user's owner's account has multi-factor (or two-factor) +authentication enabled. This is equivalent to `verified`]=] +function get.mfaEnabled(self) + return self._user and self._user._verified +end + +--[=[@p email string/nil The current user's owner's account's email address (user-accounts only).]=] +function get.email(self) + return self._user and self._user._email +end + +--[=[@p guilds Cache An iterable cache of all guilds that are visible to the client. Note that the +guilds present here correspond to which shards the client is managing. If all +shards are managed by one client, then all guilds will be present.]=] +function get.guilds(self) + return self._guilds +end + +--[=[@p users Cache An iterable cache of all users that are visible to the client. +To access a user that may exist but is not cached, use `Client:getUser`.]=] +function get.users(self) + return self._users +end + +--[=[@p privateChannels Cache An iterable cache of all private channels that are visible to the client. The +channel must exist and must be open for it to be cached here. To access a +private channel that may exist but is not cached, `User:getPrivateChannel`.]=] +function get.privateChannels(self) + return self._private_channels +end + +--[=[@p groupChannels Cache An iterable cache of all group channels that are visible to the client. Only +user-accounts should have these.]=] +function get.groupChannels(self) + return self._group_channels +end + +--[=[@p relationships Cache An iterable cache of all relationships that are visible to the client. Only +user-accounts should have these.]=] +function get.relationships(self) + return self._relationships +end + +return Client diff --git a/deps/discordia/libs/client/EventHandler.lua b/deps/discordia/libs/client/EventHandler.lua new file mode 100644 index 0000000..8457713 --- /dev/null +++ b/deps/discordia/libs/client/EventHandler.lua @@ -0,0 +1,540 @@ +local enums = require('enums') +local json = require('json') + +local channelType = enums.channelType +local insert = table.insert +local null = json.null + +local function warning(client, object, id, event) + return client:warning('Uncached %s (%s) on %s', object, id, event) +end + +local function checkReady(shard) + for _, v in pairs(shard._loading) do + if next(v) then return end + end + shard._ready = true + shard._loading = nil + collectgarbage() + local client = shard._client + client:emit('shardReady', shard._id) + for _, other in pairs(client._shards) do + if not other._ready then return end + end + return client:emit('ready') +end + +local function getChannel(client, id) + local guild = client._channel_map[id] + if guild then + return guild._text_channels:get(id) + else + return client._private_channels:get(id) or client._group_channels:get(id) + end +end + +local EventHandler = setmetatable({}, {__index = function(self, k) + self[k] = function(_, _, shard) + return shard:warning('Unhandled gateway event: %s', k) + end + return self[k] +end}) + +function EventHandler.READY(d, client, shard) + + shard:info('Received READY') + shard:emit('READY') + + shard._session_id = d.session_id + client._user = client._users:_insert(d.user) + + local guilds = client._guilds + local group_channels = client._group_channels + local private_channels = client._private_channels + local relationships = client._relationships + + for _, channel in ipairs(d.private_channels) do + if channel.type == channelType.private then + private_channels:_insert(channel) + elseif channel.type == channelType.group then + group_channels:_insert(channel) + end + end + + local loading = shard._loading + + if d.user.bot then + for _, guild in ipairs(d.guilds) do + guilds:_insert(guild) + loading.guilds[guild.id] = true + end + else + if client._options.syncGuilds then + local ids = {} + for _, guild in ipairs(d.guilds) do + guilds:_insert(guild) + if not guild.unavailable then + loading.syncs[guild.id] = true + insert(ids, guild.id) + end + end + shard:syncGuilds(ids) + else + guilds:_load(d.guilds) + end + end + + relationships:_load(d.relationships) + + for _, presence in ipairs(d.presences) do + local relationship = relationships:get(presence.user.id) + if relationship then + relationship:_loadPresence(presence) + end + end + + return checkReady(shard) + +end + +function EventHandler.RESUMED(_, client, shard) + shard:info('Received RESUMED') + return client:emit('shardResumed', shard._id) +end + +function EventHandler.GUILD_MEMBERS_CHUNK(d, client, shard) + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_MEMBERS_CHUNK') end + guild._members:_load(d.members) + if shard._loading and guild._member_count == #guild._members then + shard._loading.chunks[d.guild_id] = nil + return checkReady(shard) + end +end + +function EventHandler.GUILD_SYNC(d, client, shard) + local guild = client._guilds:get(d.id) + if not guild then return warning(client, 'Guild', d.id, 'GUILD_SYNC') end + guild._large = d.large + guild:_loadMembers(d, shard) + if shard._loading then + shard._loading.syncs[d.id] = nil + return checkReady(shard) + end +end + +function EventHandler.CHANNEL_CREATE(d, client) + local channel + local t = d.type + if t == channelType.text or t == channelType.news then + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'CHANNEL_CREATE') end + channel = guild._text_channels:_insert(d) + elseif t == channelType.voice then + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'CHANNEL_CREATE') end + channel = guild._voice_channels:_insert(d) + elseif t == channelType.private then + channel = client._private_channels:_insert(d) + elseif t == channelType.group then + channel = client._group_channels:_insert(d) + elseif t == channelType.category then + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'CHANNEL_CREATE') end + channel = guild._categories:_insert(d) + else + return client:warning('Unhandled CHANNEL_CREATE (type %s)', d.type) + end + return client:emit('channelCreate', channel) +end + +function EventHandler.CHANNEL_UPDATE(d, client) + local channel + local t = d.type + if t == channelType.text or t == channelType.news then + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'CHANNEL_UPDATE') end + channel = guild._text_channels:_insert(d) + elseif t == channelType.voice then + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'CHANNEL_UPDATE') end + channel = guild._voice_channels:_insert(d) + elseif t == channelType.private then -- private channels should never update + channel = client._private_channels:_insert(d) + elseif t == channelType.group then + channel = client._group_channels:_insert(d) + elseif t == channelType.category then + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'CHANNEL_UPDATE') end + channel = guild._categories:_insert(d) + else + return client:warning('Unhandled CHANNEL_UPDATE (type %s)', d.type) + end + return client:emit('channelUpdate', channel) +end + +function EventHandler.CHANNEL_DELETE(d, client) + local channel + local t = d.type + if t == channelType.text or t == channelType.news then + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'CHANNEL_DELETE') end + channel = guild._text_channels:_remove(d) + elseif t == channelType.voice then + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'CHANNEL_DELETE') end + channel = guild._voice_channels:_remove(d) + elseif t == channelType.private then + channel = client._private_channels:_remove(d) + elseif t == channelType.group then + channel = client._group_channels:_remove(d) + elseif t == channelType.category then + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'CHANNEL_DELETE') end + channel = guild._categories:_remove(d) + else + return client:warning('Unhandled CHANNEL_DELETE (type %s)', d.type) + end + return client:emit('channelDelete', channel) +end + +function EventHandler.CHANNEL_RECIPIENT_ADD(d, client) + local channel = client._group_channels:get(d.channel_id) + if not channel then return warning(client, 'GroupChannel', d.channel_id, 'CHANNEL_RECIPIENT_ADD') end + local user = channel._recipients:_insert(d.user) + return client:emit('recipientAdd', channel, user) +end + +function EventHandler.CHANNEL_RECIPIENT_REMOVE(d, client) + local channel = client._group_channels:get(d.channel_id) + if not channel then return warning(client, 'GroupChannel', d.channel_id, 'CHANNEL_RECIPIENT_REMOVE') end + local user = channel._recipients:_remove(d.user) + return client:emit('recipientRemove', channel, user) +end + +function EventHandler.GUILD_CREATE(d, client, shard) + if client._options.syncGuilds and not d.unavailable and not client._user._bot then + shard:syncGuilds({d.id}) + end + local guild = client._guilds:get(d.id) + if guild then + if guild._unavailable and not d.unavailable then + guild:_load(d) + guild:_makeAvailable(d) + client:emit('guildAvailable', guild) + end + if shard._loading then + shard._loading.guilds[d.id] = nil + return checkReady(shard) + end + else + guild = client._guilds:_insert(d) + return client:emit('guildCreate', guild) + end +end + +function EventHandler.GUILD_UPDATE(d, client) + local guild = client._guilds:_insert(d) + return client:emit('guildUpdate', guild) +end + +function EventHandler.GUILD_DELETE(d, client) + if d.unavailable then + local guild = client._guilds:_insert(d) + return client:emit('guildUnavailable', guild) + else + local guild = client._guilds:_remove(d) + return client:emit('guildDelete', guild) + end +end + +function EventHandler.GUILD_BAN_ADD(d, client) + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_BAN_ADD') end + local user = client._users:_insert(d.user) + return client:emit('userBan', user, guild) +end + +function EventHandler.GUILD_BAN_REMOVE(d, client) + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_BAN_REMOVE') end + local user = client._users:_insert(d.user) + return client:emit('userUnban', user, guild) +end + +function EventHandler.GUILD_EMOJIS_UPDATE(d, client) + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_EMOJIS_UPDATE') end + guild._emojis:_load(d.emojis, true) + return client:emit('emojisUpdate', guild) +end + +function EventHandler.GUILD_MEMBER_ADD(d, client) + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_MEMBER_ADD') end + local member = guild._members:_insert(d) + guild._member_count = guild._member_count + 1 + return client:emit('memberJoin', member) +end + +function EventHandler.GUILD_MEMBER_UPDATE(d, client) + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_MEMBER_UPDATE') end + local member = guild._members:_insert(d) + return client:emit('memberUpdate', member) +end + +function EventHandler.GUILD_MEMBER_REMOVE(d, client) + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_MEMBER_REMOVE') end + local member = guild._members:_remove(d) + guild._member_count = guild._member_count - 1 + return client:emit('memberLeave', member) +end + +function EventHandler.GUILD_ROLE_CREATE(d, client) + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_ROLE_CREATE') end + local role = guild._roles:_insert(d.role) + return client:emit('roleCreate', role) +end + +function EventHandler.GUILD_ROLE_UPDATE(d, client) + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_ROLE_UPDATE') end + local role = guild._roles:_insert(d.role) + return client:emit('roleUpdate', role) +end + +function EventHandler.GUILD_ROLE_DELETE(d, client) -- role object not provided + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'GUILD_ROLE_DELETE') end + local role = guild._roles:_delete(d.role_id) + if not role then return warning(client, 'Role', d.role_id, 'GUILD_ROLE_DELETE') end + return client:emit('roleDelete', role) +end + +function EventHandler.MESSAGE_CREATE(d, client) + local channel = getChannel(client, d.channel_id) + if not channel then return warning(client, 'TextChannel', d.channel_id, 'MESSAGE_CREATE') end + local message = channel._messages:_insert(d) + return client:emit('messageCreate', message) +end + +function EventHandler.MESSAGE_UPDATE(d, client) -- may not contain the whole message + local channel = getChannel(client, d.channel_id) + if not channel then return warning(client, 'TextChannel', d.channel_id, 'MESSAGE_UPDATE') end + local message = channel._messages:get(d.id) + if message then + message:_setOldContent(d) + message:_load(d) + return client:emit('messageUpdate', message) + else + return client:emit('messageUpdateUncached', channel, d.id) + end +end + +function EventHandler.MESSAGE_DELETE(d, client) -- message object not provided + local channel = getChannel(client, d.channel_id) + if not channel then return warning(client, 'TextChannel', d.channel_id, 'MESSAGE_DELETE') end + local message = channel._messages:_delete(d.id) + if message then + return client:emit('messageDelete', message) + else + return client:emit('messageDeleteUncached', channel, d.id) + end +end + +function EventHandler.MESSAGE_DELETE_BULK(d, client) + local channel = getChannel(client, d.channel_id) + if not channel then return warning(client, 'TextChannel', d.channel_id, 'MESSAGE_DELETE_BULK') end + for _, id in ipairs(d.ids) do + local message = channel._messages:_delete(id) + if message then + client:emit('messageDelete', message) + else + client:emit('messageDeleteUncached', channel, id) + end + end +end + +function EventHandler.MESSAGE_REACTION_ADD(d, client) + local channel = getChannel(client, d.channel_id) + if not channel then return warning(client, 'TextChannel', d.channel_id, 'MESSAGE_REACTION_ADD') end + local message = channel._messages:get(d.message_id) + if message then + local reaction = message:_addReaction(d) + return client:emit('reactionAdd', reaction, d.user_id) + else + local k = d.emoji.id ~= null and d.emoji.id or d.emoji.name + return client:emit('reactionAddUncached', channel, d.message_id, k, d.user_id) + end +end + +function EventHandler.MESSAGE_REACTION_REMOVE(d, client) + local channel = getChannel(client, d.channel_id) + if not channel then return warning(client, 'TextChannel', d.channel_id, 'MESSAGE_REACTION_REMOVE') end + local message = channel._messages:get(d.message_id) + if message then + local reaction = message:_removeReaction(d) + if not reaction then -- uncached reaction? + local k = d.emoji.id ~= null and d.emoji.id or d.emoji.name + return warning(client, 'Reaction', k, 'MESSAGE_REACTION_REMOVE') + end + return client:emit('reactionRemove', reaction, d.user_id) + else + local k = d.emoji.id ~= null and d.emoji.id or d.emoji.name + return client:emit('reactionRemoveUncached', channel, d.message_id, k, d.user_id) + end +end + +function EventHandler.MESSAGE_REACTION_REMOVE_ALL(d, client) + local channel = getChannel(client, d.channel_id) + if not channel then return warning(client, 'TextChannel', d.channel_id, 'MESSAGE_REACTION_REMOVE_ALL') end + local message = channel._messages:get(d.message_id) + if message then + local reactions = message._reactions + if reactions then + for reaction in reactions:iter() do + reaction._count = 0 + end + message._reactions = nil + end + return client:emit('reactionRemoveAll', message) + else + return client:emit('reactionRemoveAllUncached', channel, d.message_id) + end +end + +function EventHandler.CHANNEL_PINS_UPDATE(d, client) + local channel = getChannel(client, d.channel_id) + if not channel then return warning(client, 'TextChannel', d.channel_id, 'CHANNEL_PINS_UPDATE') end + return client:emit('pinsUpdate', channel) +end + +function EventHandler.PRESENCE_UPDATE(d, client) -- may have incomplete data + local user = client._users:get(d.user.id) + if user then + user:_load(d.user) + end + if d.guild_id then + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'PRESENCE_UPDATE') end + local member + if client._options.cacheAllMembers then + member = guild._members:get(d.user.id) + if not member then return end -- still loading or member left + else + if d.status == 'offline' then -- uncache offline members + member = guild._members:_delete(d.user.id) + else + if d.user.username then -- member was offline + member = guild._members:_insert(d) + elseif user then -- member was invisible, user is still cached + member = guild._members:_insert(d) + member._user = user + end + end + end + if member then + member:_loadPresence(d) + return client:emit('presenceUpdate', member) + end + else + local relationship = client._relationships:get(d.user.id) + if relationship then + relationship:_loadPresence(d) + return client:emit('relationshipUpdate', relationship) + end + end +end + +function EventHandler.RELATIONSHIP_ADD(d, client) + local relationship = client._relationships:_insert(d) + return client:emit('relationshipAdd', relationship) +end + +function EventHandler.RELATIONSHIP_REMOVE(d, client) + local relationship = client._relationships:_remove(d) + return client:emit('relationshipRemove', relationship) +end + +function EventHandler.TYPING_START(d, client) + return client:emit('typingStart', d.user_id, d.channel_id, d.timestamp) +end + +function EventHandler.USER_UPDATE(d, client) + client._user:_load(d) + return client:emit('userUpdate', client._user) +end + +local function load(obj, d) + for k, v in pairs(d) do obj[k] = v end +end + +function EventHandler.VOICE_STATE_UPDATE(d, client) + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'VOICE_STATE_UPDATE') end + local member = d.member and guild._members:_insert(d.member) or guild._members:get(d.user_id) + if not member then return warning(client, 'Member', d.user_id, 'VOICE_STATE_UPDATE') end + local states = guild._voice_states + local channels = guild._voice_channels + local new_channel_id = d.channel_id + local state = states[d.user_id] + if state then -- user is already connected + local old_channel_id = state.channel_id + load(state, d) + if new_channel_id ~= null then -- state changed, but user has not disconnected + if new_channel_id == old_channel_id then -- user did not change channels + client:emit('voiceUpdate', member) + else -- user changed channels + local old = channels:get(old_channel_id) + local new = channels:get(new_channel_id) + if d.user_id == client._user._id then -- move connection to new channel + local connection = old._connection + if connection then + new._connection = connection + old._connection = nil + connection._channel = new + connection:_continue(true) + end + end + client:emit('voiceChannelLeave', member, old) + client:emit('voiceChannelJoin', member, new) + end + else -- user has disconnected + states[d.user_id] = nil + local old = channels:get(old_channel_id) + client:emit('voiceChannelLeave', member, old) + client:emit('voiceDisconnect', member) + end + else -- user has connected + states[d.user_id] = d + local new = channels:get(new_channel_id) + client:emit('voiceConnect', member) + client:emit('voiceChannelJoin', member, new) + end +end + +function EventHandler.VOICE_SERVER_UPDATE(d, client) + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'VOICE_SERVER_UPDATE') end + local state = guild._voice_states[client._user._id] + if not state then return client:warning('Voice state not initialized before VOICE_SERVER_UPDATE') end + load(state, d) + local channel = guild._voice_channels:get(state.channel_id) + if not channel then return warning(client, 'GuildVoiceChannel', state.channel_id, 'VOICE_SERVER_UPDATE') end + local connection = channel._connection + if not connection then return client:warning('Voice connection not initialized before VOICE_SERVER_UPDATE') end + return client._voice:_prepareConnection(state, connection) +end + +function EventHandler.WEBHOOKS_UPDATE(d, client) -- webhook object is not provided + local guild = client._guilds:get(d.guild_id) + if not guild then return warning(client, 'Guild', d.guild_id, 'WEBHOOKS_UDPATE') end + local channel = guild._text_channels:get(d.channel_id) + if not channel then return warning(client, 'TextChannel', d.channel_id, 'WEBHOOKS_UPDATE') end + return client:emit('webhooksUpdate', channel) +end + +return EventHandler diff --git a/deps/discordia/libs/client/Resolver.lua b/deps/discordia/libs/client/Resolver.lua new file mode 100644 index 0000000..0ee2648 --- /dev/null +++ b/deps/discordia/libs/client/Resolver.lua @@ -0,0 +1,202 @@ +local fs = require('fs') +local ffi = require('ffi') +local ssl = require('openssl') +local class = require('class') +local enums = require('enums') + +local permission = enums.permission +local actionType = enums.actionType +local messageFlag = enums.messageFlag +local base64 = ssl.base64 +local readFileSync = fs.readFileSync +local classes = class.classes +local isInstance = class.isInstance +local isObject = class.isObject +local insert = table.insert +local format = string.format + +local Resolver = {} + +local istype = ffi.istype +local int64_t = ffi.typeof('int64_t') +local uint64_t = ffi.typeof('uint64_t') + +local function int(obj) + local t = type(obj) + if t == 'string' then + if tonumber(obj) then + return obj + end + elseif t == 'cdata' then + if istype(int64_t, obj) or istype(uint64_t, obj) then + return tostring(obj):match('%d*') + end + elseif t == 'number' then + return format('%i', obj) + elseif isInstance(obj, classes.Date) then + return obj:toSnowflake() + end +end + +function Resolver.userId(obj) + if isObject(obj) then + if isInstance(obj, classes.User) then + return obj.id + elseif isInstance(obj, classes.Member) then + return obj.user.id + elseif isInstance(obj, classes.Message) then + return obj.author.id + elseif isInstance(obj, classes.Guild) then + return obj.ownerId + end + end + return int(obj) +end + +function Resolver.messageId(obj) + if isInstance(obj, classes.Message) then + return obj.id + end + return int(obj) +end + +function Resolver.channelId(obj) + if isInstance(obj, classes.Channel) then + return obj.id + end + return int(obj) +end + +function Resolver.roleId(obj) + if isInstance(obj, classes.Role) then + return obj.id + end + return int(obj) +end + +function Resolver.emojiId(obj) + if isInstance(obj, classes.Emoji) then + return obj.id + elseif isInstance(obj, classes.Reaction) then + return obj.emojiId + elseif isInstance(obj, classes.Activity) then + return obj.emojiId + end + return int(obj) +end + +function Resolver.guildId(obj) + if isInstance(obj, classes.Guild) then + return obj.id + end + return int(obj) +end + +function Resolver.entryId(obj) + if isInstance(obj, classes.AuditLogEntry) then + return obj.id + end + return int(obj) +end + +function Resolver.messageIds(objs) + local ret = {} + if isInstance(objs, classes.Iterable) then + for obj in objs:iter() do + insert(ret, Resolver.messageId(obj)) + end + elseif type(objs) == 'table' then + for _, obj in pairs(objs) do + insert(ret, Resolver.messageId(obj)) + end + end + return ret +end + +function Resolver.roleIds(objs) + local ret = {} + if isInstance(objs, classes.Iterable) then + for obj in objs:iter() do + insert(ret, Resolver.roleId(obj)) + end + elseif type(objs) == 'table' then + for _, obj in pairs(objs) do + insert(ret, Resolver.roleId(obj)) + end + end + return ret +end + +function Resolver.emoji(obj) + if isInstance(obj, classes.Emoji) then + return obj.hash + elseif isInstance(obj, classes.Reaction) then + return obj.emojiHash + elseif isInstance(obj, classes.Activity) then + return obj.emojiHash + end + return tostring(obj) +end + +function Resolver.color(obj) + if isInstance(obj, classes.Color) then + return obj.value + end + return tonumber(obj) +end + +function Resolver.permissions(obj) + if isInstance(obj, classes.Permissions) then + return obj.value + end + return tonumber(obj) +end + +function Resolver.permission(obj) + local t = type(obj) + local n = nil + if t == 'string' then + n = permission[obj] + elseif t == 'number' then + n = permission(obj) and obj + end + return n +end + +function Resolver.actionType(obj) + local t = type(obj) + local n = nil + if t == 'string' then + n = actionType[obj] + elseif t == 'number' then + n = actionType(obj) and obj + end + return n +end + +function Resolver.messageFlag(obj) + local t = type(obj) + local n = nil + if t == 'string' then + n = messageFlag[obj] + elseif t == 'number' then + n = messageFlag(obj) and obj + end + return n +end + +function Resolver.base64(obj) + if type(obj) == 'string' then + if obj:find('data:.*;base64,') == 1 then + return obj + end + local data, err = readFileSync(obj) + if not data then + return nil, err + end + return 'data:;base64,' .. base64(data) + end + return nil +end + +return Resolver diff --git a/deps/discordia/libs/client/Shard.lua b/deps/discordia/libs/client/Shard.lua new file mode 100644 index 0000000..51ab5ad --- /dev/null +++ b/deps/discordia/libs/client/Shard.lua @@ -0,0 +1,252 @@ +local json = require('json') +local timer = require('timer') + +local EventHandler = require('client/EventHandler') +local WebSocket = require('client/WebSocket') + +local constants = require('constants') +local enums = require('enums') + +local logLevel = enums.logLevel +local min, max, random = math.min, math.max, math.random +local null = json.null +local format = string.format +local sleep = timer.sleep +local setInterval, clearInterval = timer.setInterval, timer.clearInterval +local wrap = coroutine.wrap + +local ID_DELAY = constants.ID_DELAY + +local DISPATCH = 0 +local HEARTBEAT = 1 +local IDENTIFY = 2 +local STATUS_UPDATE = 3 +local VOICE_STATE_UPDATE = 4 +-- local VOICE_SERVER_PING = 5 -- TODO +local RESUME = 6 +local RECONNECT = 7 +local REQUEST_GUILD_MEMBERS = 8 +local INVALID_SESSION = 9 +local HELLO = 10 +local HEARTBEAT_ACK = 11 +local GUILD_SYNC = 12 + +local ignore = { + ['CALL_DELETE'] = true, + ['CHANNEL_PINS_ACK'] = true, + ['GUILD_INTEGRATIONS_UPDATE'] = true, + ['MESSAGE_ACK'] = true, + ['PRESENCES_REPLACE'] = true, + ['USER_SETTINGS_UPDATE'] = true, + ['USER_GUILD_SETTINGS_UPDATE'] = true, + ['SESSIONS_REPLACE'] = true, +} + +local Shard = require('class')('Shard', WebSocket) + +function Shard:__init(id, client) + WebSocket.__init(self, client) + self._id = id + self._client = client + self._backoff = 1000 +end + +for name in pairs(logLevel) do + Shard[name] = function(self, fmt, ...) + local client = self._client + return client[name](client, format('Shard %i : %s', self._id, fmt), ...) + end +end + +function Shard:__tostring() + return format('Shard: %i', self._id) +end + +local function getReconnectTime(self, n, m) + return self._backoff * (n + random() * (m - n)) +end + +local function incrementReconnectTime(self) + self._backoff = min(self._backoff * 2, 60000) +end + +local function decrementReconnectTime(self) + self._backoff = max(self._backoff / 2, 1000) +end + +function Shard:handleDisconnect(url, path) + self._client:emit('shardDisconnect', self._id) + if self._reconnect then + self:info('Reconnecting...') + return self:connect(url, path) + elseif self._reconnect == nil and self._client._options.autoReconnect then + local backoff = getReconnectTime(self, 0.9, 1.1) + incrementReconnectTime(self) + self:info('Reconnecting after %i ms...', backoff) + sleep(backoff) + return self:connect(url, path) + end +end + +function Shard:handlePayload(payload) + + local client = self._client + + local s = payload.s + local t = payload.t + local d = payload.d + local op = payload.op + + if t ~= null then + self:debug('WebSocket OP %s : %s : %s', op, t, s) + else + self:debug('WebSocket OP %s', op) + end + + if op == DISPATCH then + + self._seq = s + if not ignore[t] then + EventHandler[t](d, client, self) + end + + elseif op == HEARTBEAT then + + self:heartbeat() + + elseif op == RECONNECT then + + self:warning('Discord has requested a reconnection') + self:disconnect(true) + + elseif op == INVALID_SESSION then + + if payload.d and self._session_id then + self:info('Session invalidated, resuming...') + self:resume() + else + self:info('Session invalidated, re-identifying...') + sleep(random(1000, 5000)) + self:identify() + end + + elseif op == HELLO then + + self:info('Received HELLO') + self:startHeartbeat(d.heartbeat_interval) + if self._session_id then + self:resume() + else + self:identify() + end + + elseif op == HEARTBEAT_ACK then + + client:emit('heartbeat', self._id, self._sw.milliseconds) + + elseif op then + + self:warning('Unhandled WebSocket payload OP %i', op) + + end + +end + +local function loop(self) + decrementReconnectTime(self) + return wrap(self.heartbeat)(self) +end + +function Shard:startHeartbeat(interval) + if self._heartbeat then + clearInterval(self._heartbeat) + end + self._heartbeat = setInterval(interval, loop, self) +end + +function Shard:stopHeartbeat() + if self._heartbeat then + clearInterval(self._heartbeat) + end + self._heartbeat = nil +end + +function Shard:identifyWait() + if self:waitFor('READY', 1.5 * ID_DELAY) then + return sleep(ID_DELAY) + end +end + +function Shard:heartbeat() + self._sw:reset() + return self:_send(HEARTBEAT, self._seq or json.null) +end + +function Shard:identify() + + local client = self._client + local mutex = client._mutex + local options = client._options + + mutex:lock() + wrap(function() + self:identifyWait() + mutex:unlock() + end)() + + self._seq = nil + self._session_id = nil + self._ready = false + self._loading = {guilds = {}, chunks = {}, syncs = {}} + + return self:_send(IDENTIFY, { + token = client._token, + properties = { + ['$os'] = jit.os, + ['$browser'] = 'Discordia', + ['$device'] = 'Discordia', + ['$referrer'] = '', + ['$referring_domain'] = '', + }, + compress = options.compress, + large_threshold = options.largeThreshold, + shard = {self._id, client._total_shard_count}, + presence = next(client._presence) and client._presence, + }, true) + +end + +function Shard:resume() + return self:_send(RESUME, { + token = self._client._token, + session_id = self._session_id, + seq = self._seq + }) +end + +function Shard:requestGuildMembers(id) + return self:_send(REQUEST_GUILD_MEMBERS, { + guild_id = id, + query = '', + limit = 0, + }) +end + +function Shard:updateStatus(presence) + return self:_send(STATUS_UPDATE, presence) +end + +function Shard:updateVoice(guild_id, channel_id, self_mute, self_deaf) + return self:_send(VOICE_STATE_UPDATE, { + guild_id = guild_id, + channel_id = channel_id or null, + self_mute = self_mute or false, + self_deaf = self_deaf or false, + }) +end + +function Shard:syncGuilds(ids) + return self:_send(GUILD_SYNC, ids) +end + +return Shard diff --git a/deps/discordia/libs/client/WebSocket.lua b/deps/discordia/libs/client/WebSocket.lua new file mode 100644 index 0000000..412d0d6 --- /dev/null +++ b/deps/discordia/libs/client/WebSocket.lua @@ -0,0 +1,121 @@ +local json = require('json') +local miniz = require('miniz') +local Mutex = require('utils/Mutex') +local Emitter = require('utils/Emitter') +local Stopwatch = require('utils/Stopwatch') + +local websocket = require('coro-websocket') +local constants = require('constants') + +local inflate = miniz.inflate +local encode, decode, null = json.encode, json.decode, json.null +local ws_parseUrl, ws_connect = websocket.parseUrl, websocket.connect + +local GATEWAY_DELAY = constants.GATEWAY_DELAY + +local TEXT = 1 +local BINARY = 2 +local CLOSE = 8 + +local function connect(url, path) + local options = assert(ws_parseUrl(url)) + options.pathname = path + return assert(ws_connect(options)) +end + +local WebSocket = require('class')('WebSocket', Emitter) + +function WebSocket:__init(parent) + Emitter.__init(self) + self._parent = parent + self._mutex = Mutex() + self._sw = Stopwatch() +end + +function WebSocket:connect(url, path) + + local success, res, read, write = pcall(connect, url, path) + + if success then + self._read = read + self._write = write + self._reconnect = nil + self:info('Connected to %s', url) + local parent = self._parent + for message in self._read do + local payload, str = self:parseMessage(message) + if not payload then break end + parent:emit('raw', str) + if self.handlePayload then -- virtual method + self:handlePayload(payload) + end + end + self:info('Disconnected') + else + self:error('Could not connect to %s (%s)', url, res) -- TODO: get new url? + end + + self._read = nil + self._write = nil + self._identified = nil + + if self.stopHeartbeat then -- virtual method + self:stopHeartbeat() + end + + if self.handleDisconnect then -- virtual method + return self:handleDisconnect(url, path) + end + +end + +function WebSocket:parseMessage(message) + + local opcode = message.opcode + local payload = message.payload + + if opcode == TEXT then + + return decode(payload, 1, null), payload + + elseif opcode == BINARY then + + payload = inflate(payload, 1) + return decode(payload, 1, null), payload + + elseif opcode == CLOSE then + + local code, i = ('>H'):unpack(payload) + local msg = #payload > i and payload:sub(i) or 'Connection closed' + self:warning('%i - %s', code, msg) + return nil + + end + +end + +function WebSocket:_send(op, d, identify) + self._mutex:lock() + local success, err + if identify or self._session_id then + if self._write then + success, err = self._write {opcode = TEXT, payload = encode {op = op, d = d}} + else + success, err = false, 'Not connected to gateway' + end + else + success, err = false, 'Invalid session' + end + self._mutex:unlockAfter(GATEWAY_DELAY) + return success, err +end + +function WebSocket:disconnect(reconnect) + if not self._write then return end + self._reconnect = not not reconnect + self._write() + self._read = nil + self._write = nil +end + +return WebSocket diff --git a/deps/discordia/libs/constants.lua b/deps/discordia/libs/constants.lua new file mode 100644 index 0000000..5836d94 --- /dev/null +++ b/deps/discordia/libs/constants.lua @@ -0,0 +1,17 @@ +return { + CACHE_AGE = 3600, -- seconds + ID_DELAY = 5000, -- milliseconds + GATEWAY_DELAY = 500, -- milliseconds, + DISCORD_EPOCH = 1420070400000, -- milliseconds + GATEWAY_VERSION = 6, + DEFAULT_AVATARS = 5, + ZWSP = '\226\128\139', + NS_PER_US = 1000, + US_PER_MS = 1000, + MS_PER_S = 1000, + S_PER_MIN = 60, + MIN_PER_HOUR = 60, + HOUR_PER_DAY = 24, + DAY_PER_WEEK = 7, + GATEWAY_VERSION_VOICE = 3, +} diff --git a/deps/discordia/libs/containers/Activity.lua b/deps/discordia/libs/containers/Activity.lua new file mode 100644 index 0000000..74e92c1 --- /dev/null +++ b/deps/discordia/libs/containers/Activity.lua @@ -0,0 +1,148 @@ +--[=[ +@c Activity +@d Represents a Discord user's presence data, either plain game or streaming presence or a rich presence. +Most if not all properties may be nil. +]=] + +local Container = require('containers/abstract/Container') + +local format = string.format + +local Activity, get = require('class')('Activity', Container) + +function Activity:__init(data, parent) + Container.__init(self, data, parent) + return self:_loadMore(data) +end + +function Activity:_load(data) + Container._load(self, data) + return self:_loadMore(data) +end + +function Activity:_loadMore(data) + local timestamps = data.timestamps + self._start = timestamps and timestamps.start + self._stop = timestamps and timestamps['end'] -- thanks discord + local assets = data.assets + self._small_text = assets and assets.small_text + self._large_text = assets and assets.large_text + self._small_image = assets and assets.small_image + self._large_image = assets and assets.large_image + local party = data.party + self._party_id = party and party.id + self._party_size = party and party.size and party.size[1] + self._party_max = party and party.size and party.size[2] + local emoji = data.emoji + self._emoji_name = emoji and emoji.name + self._emoji_id = emoji and emoji.id + self._emoji_animated = emoji and emoji.animated +end + +--[=[@p start number/nil The Unix timestamp for when this Rich Presence activity was started.]=] +function get.start(self) + return self._start +end + +--[=[@p stop number/nil The Unix timestamp for when this Rich Presence activity was stopped.]=] +function get.stop(self) + return self._stop +end + +--[=[@p name string/nil The game that the user is currently playing.]=] +function get.name(self) + return self._name +end + +--[=[@p type number/nil The type of user's game status. See the `activityType` +enumeration for a human-readable representation.]=] +function get.type(self) + return self._type +end + +--[=[@p url string/nil The URL that is set for a user's streaming game status.]=] +function get.url(self) + return self._url +end + +--[=[@p applicationId string/nil The application id controlling this Rich Presence activity.]=] +function get.applicationId(self) + return self._application_id +end + +--[=[@p state string/nil string for the Rich Presence state section.]=] +function get.state(self) + return self._state +end + +--[=[@p details string/nil string for the Rich Presence details section.]=] +function get.details(self) + return self._details +end + +--[=[@p textSmall string/nil string for the Rich Presence small image text.]=] +function get.textSmall(self) + return self._small_text +end + +--[=[@p textLarge string/nil string for the Rich Presence large image text.]=] +function get.textLarge(self) + return self._large_text +end + +--[=[@p imageSmall string/nil URL for the Rich Presence small image.]=] +function get.imageSmall(self) + return self._small_image +end + +--[=[@p imageLarge string/nil URL for the Rich Presence large image.]=] +function get.imageLarge(self) + return self._large_image +end + +--[=[@p partyId string/nil Party id for this Rich Presence.]=] +function get.partyId(self) + return self._party_id +end + +--[=[@p partySize number/nil Size of the Rich Presence party.]=] +function get.partySize(self) + return self._party_size +end + +--[=[@p partyMax number/nil Max size for the Rich Presence party.]=] +function get.partyMax(self) + return self._party_max +end + +--[=[@p emojiId string/nil The ID of the emoji used in this presence if one is +set and if it is a custom emoji.]=] +function get.emojiId(self) + return self._emoji_id +end + +--[=[@p emojiName string/nil The name of the emoji used in this presence if one +is set and if it has a custom emoji. This will be the raw string for a standard emoji.]=] +function get.emojiName(self) + return self._emoji_name +end + +--[=[@p emojiHash string/nil The discord hash for the emoji used in this presence if one is +set. This will be the raw string for a standard emoji.]=] +function get.emojiHash(self) + if self._emoji_id then + return self._emoji_name .. ':' .. self._emoji_id + else + return self._emoji_name + end +end + +--[=[@p emojiURL string/nil string The URL that can be used to view a full +version of the emoji used in this activity if one is set and if it is a custom emoji.]=] +function get.emojiURL(self) + local id = self._emoji_id + local ext = self._emoji_animated and 'gif' or 'png' + return id and format('https://cdn.discordapp.com/emojis/%s.%s', id, ext) or nil +end + +return Activity diff --git a/deps/discordia/libs/containers/AuditLogEntry.lua b/deps/discordia/libs/containers/AuditLogEntry.lua new file mode 100644 index 0000000..c82fe90 --- /dev/null +++ b/deps/discordia/libs/containers/AuditLogEntry.lua @@ -0,0 +1,227 @@ +--[=[ +@c AuditLogEntry x Snowflake +@d Represents an entry made into a guild's audit log. +]=] + +local Snowflake = require('containers/abstract/Snowflake') + +local enums = require('enums') +local actionType = enums.actionType + +local AuditLogEntry, get = require('class')('AuditLogEntry', Snowflake) + +function AuditLogEntry:__init(data, parent) + Snowflake.__init(self, data, parent) + if data.changes then + for i, change in ipairs(data.changes) do + data.changes[change.key] = change + data.changes[i] = nil + change.key = nil + change.old = change.old_value + change.new = change.new_value + change.old_value = nil + change.new_value = nil + end + self._changes = data.changes + end + self._options = data.options +end + +--[=[ +@m getBeforeAfter +@t mem +@r table +@r table +@d Returns two tables of the target's properties before the change, and after the change. +]=] +function AuditLogEntry:getBeforeAfter() + local before, after = {}, {} + for k, change in pairs(self._changes) do + before[k], after[k] = change.old, change.new + end + return before, after +end + +local function unknown(self) + return nil, 'unknown audit log action type: ' .. self._action_type +end + +local targets = setmetatable({ + + [actionType.guildUpdate] = function(self) + return self._parent + end, + + [actionType.channelCreate] = function(self) + return self._parent:getChannel(self._target_id) + end, + + [actionType.channelUpdate] = function(self) + return self._parent:getChannel(self._target_id) + end, + + [actionType.channelDelete] = function(self) + return self._parent:getChannel(self._target_id) + end, + + [actionType.channelOverwriteCreate] = function(self) + return self._parent:getChannel(self._target_id) + end, + + [actionType.channelOverwriteUpdate] = function(self) + return self._parent:getChannel(self._target_id) + end, + + [actionType.channelOverwriteDelete] = function(self) + return self._parent:getChannel(self._target_id) + end, + + [actionType.memberKick] = function(self) + return self._parent._parent:getUser(self._target_id) + end, + + [actionType.memberPrune] = function() + return nil + end, + + [actionType.memberBanAdd] = function(self) + return self._parent._parent:getUser(self._target_id) + end, + + [actionType.memberBanRemove] = function(self) + return self._parent._parent:getUser(self._target_id) + end, + + [actionType.memberUpdate] = function(self) + return self._parent:getMember(self._target_id) + end, + + [actionType.memberRoleUpdate] = function(self) + return self._parent:getMember(self._target_id) + end, + + [actionType.roleCreate] = function(self) + return self._parent:getRole(self._target_id) + end, + + [actionType.roleUpdate] = function(self) + return self._parent:getRole(self._target_id) + end, + + [actionType.roleDelete] = function(self) + return self._parent:getRole(self._target_id) + end, + + [actionType.inviteCreate] = function() + return nil + end, + + [actionType.inviteUpdate] = function() + return nil + end, + + [actionType.inviteDelete] = function() + return nil + end, + + [actionType.webhookCreate] = function(self) + return self._parent._parent._webhooks:get(self._target_id) + end, + + [actionType.webhookUpdate] = function(self) + return self._parent._parent._webhooks:get(self._target_id) + end, + + [actionType.webhookDelete] = function(self) + return self._parent._parent._webhooks:get(self._target_id) + end, + + [actionType.emojiCreate] = function(self) + return self._parent:getEmoji(self._target_id) + end, + + [actionType.emojiUpdate] = function(self) + return self._parent:getEmoji(self._target_id) + end, + + [actionType.emojiDelete] = function(self) + return self._parent:getEmoji(self._target_id) + end, + + [actionType.messageDelete] = function(self) + return self._parent._parent:getUser(self._target_id) + end, + +}, {__index = function() return unknown end}) + +--[=[ +@m getTarget +@t http? +@r * +@d Gets the target object of the affected entity. The returned object can be: [[Guild]], +[[GuildChannel]], [[User]], [[Member]], [[Role]], [[Webhook]], [[Emoji]], nil +]=] +function AuditLogEntry:getTarget() + return targets[self._action_type](self) +end + +--[=[ +@m getUser +@t http? +@r User +@d Gets the user who performed the changes. +]=] +function AuditLogEntry:getUser() + return self._parent._parent:getUser(self._user_id) +end + +--[=[ +@m getMember +@t http? +@r Member/nil +@d Gets the member object of the user who performed the changes. +]=] +function AuditLogEntry:getMember() + return self._parent:getMember(self._user_id) +end + +--[=[@p changes table/nil A table of audit log change objects. The key represents +the property of the changed target and the value contains a table of `new` and +possibly `old`, representing the property's new and old value.]=] +function get.changes(self) + return self._changes +end + +--[=[@p options table/nil A table of optional audit log information.]=] +function get.options(self) + return self._options +end + +--[=[@p actionType number The action type. Use the `actionType `enumeration +for a human-readable representation.]=] +function get.actionType(self) + return self._action_type +end + +--[=[@p targetId string/nil The Snowflake ID of the affected entity. Will +be `nil` for certain targets.]=] +function get.targetId(self) + return self._target_id +end + +--[=[@p userId string The Snowflake ID of the user who commited the action.]=] +function get.userId(self) + return self._user_id +end + +--[=[@p reason string/nil The reason provided by the user for the change.]=] +function get.reason(self) + return self._reason +end + +--[=[@p guild Guild The guild in which this audit log entry was found.]=] +function get.guild(self) + return self._parent +end + +return AuditLogEntry diff --git a/deps/discordia/libs/containers/Ban.lua b/deps/discordia/libs/containers/Ban.lua new file mode 100644 index 0000000..09d37d5 --- /dev/null +++ b/deps/discordia/libs/containers/Ban.lua @@ -0,0 +1,52 @@ +--[=[ +@c Ban x Container +@d Represents a Discord guild ban. Essentially a combination of the banned user and +a reason explaining the ban, if one was provided. +]=] + +local Container = require('containers/abstract/Container') + +local Ban, get = require('class')('Ban', Container) + +function Ban:__init(data, parent) + Container.__init(self, data, parent) + self._user = self.client._users:_insert(data.user) +end + +--[=[ +@m __hash +@r string +@d Returns `Ban.user.id` +]=] +function Ban:__hash() + return self._user._id +end + +--[=[ +@m delete +@t http +@r boolean +@d Deletes the ban object, unbanning the corresponding user. +Equivalent to `Ban.guild:unbanUser(Ban.user)`. +]=] +function Ban:delete() + return self._parent:unbanUser(self._user) +end + +--[=[@p reason string/nil The reason for the ban, if one was set. This should be from 1 to 512 characters +in length.]=] +function get.reason(self) + return self._reason +end + +--[=[@p guild Guild The guild in which this ban object exists.]=] +function get.guild(self) + return self._parent +end + +--[=[@p user User The user that this ban object represents.]=] +function get.user(self) + return self._user +end + +return Ban diff --git a/deps/discordia/libs/containers/Emoji.lua b/deps/discordia/libs/containers/Emoji.lua new file mode 100644 index 0000000..52c4de6 --- /dev/null +++ b/deps/discordia/libs/containers/Emoji.lua @@ -0,0 +1,168 @@ +--[=[ +@c Emoji x Snowflake +@d Represents a custom emoji object usable in message content and reactions. +Standard unicode emojis do not have a class; they are just strings. +]=] + +local Snowflake = require('containers/abstract/Snowflake') +local Resolver = require('client/Resolver') +local ArrayIterable = require('iterables/ArrayIterable') +local json = require('json') + +local format = string.format + +local Emoji, get = require('class')('Emoji', Snowflake) + +function Emoji:__init(data, parent) + Snowflake.__init(self, data, parent) + self.client._emoji_map[self._id] = parent + return self:_loadMore(data) +end + +function Emoji:_load(data) + Snowflake._load(self, data) + return self:_loadMore(data) +end + +function Emoji:_loadMore(data) + if data.roles then + local roles = #data.roles > 0 and data.roles or nil + if self._roles then + self._roles._array = roles + else + self._roles_raw = roles + end + end +end + +function Emoji:_modify(payload) + local data, err = self.client._api:modifyGuildEmoji(self._parent._id, self._id, payload) + if data then + self:_load(data) + return true + else + return false, err + end +end + +--[=[ +@m setName +@t http +@p name string +@r boolean +@d Sets the emoji's name. The name must be between 2 and 32 characters in length. +]=] +function Emoji:setName(name) + return self:_modify({name = name or json.null}) +end + +--[=[ +@m setRoles +@t http +@p roles Role-ID-Resolvables +@r boolean +@d Sets the roles that can use the emoji. +]=] +function Emoji:setRoles(roles) + roles = Resolver.roleIds(roles) + return self:_modify({roles = roles or json.null}) +end + +--[=[ +@m delete +@t http +@r boolean +@d Permanently deletes the emoji. This cannot be undone! +]=] +function Emoji:delete() + local data, err = self.client._api:deleteGuildEmoji(self._parent._id, self._id) + if data then + local cache = self._parent._emojis + if cache then + cache:_delete(self._id) + end + return true + else + return false, err + end +end + +--[=[ +@m hasRole +@t mem +@p id Role-ID-Resolvable +@r boolean +@d Returns whether or not the provided role is allowed to use the emoji. +]=] +function Emoji:hasRole(id) + id = Resolver.roleId(id) + local roles = self._roles and self._roles._array or self._roles_raw + if roles then + for _, v in ipairs(roles) do + if v == id then + return true + end + end + end + return false +end + +--[=[@p name string The name of the emoji.]=] +function get.name(self) + return self._name +end + +--[=[@p guild Guild The guild in which the emoji exists.]=] +function get.guild(self) + return self._parent +end + +--[=[@p mentionString string A string that, when included in a message content, may resolve as an emoji image +in the official Discord client.]=] +function get.mentionString(self) + local fmt = self._animated and '' or '<:%s>' + return format(fmt, self.hash) +end + +--[=[@p url string The URL that can be used to view a full version of the emoji.]=] +function get.url(self) + local ext = self._animated and 'gif' or 'png' + return format('https://cdn.discordapp.com/emojis/%s.%s', self._id, ext) +end + +--[=[@p managed boolean Whether this emoji is managed by an integration such as Twitch or YouTube.]=] +function get.managed(self) + return self._managed +end + +--[=[@p requireColons boolean Whether this emoji requires colons to be used in the official Discord client.]=] +function get.requireColons(self) + return self._require_colons +end + +--[=[@p hash string String with the format `name:id`, used in HTTP requests. +This is different from `Emoji:__hash`, which returns only the Snowflake ID. +]=] +function get.hash(self) + return self._name .. ':' .. self._id +end + +--[=[@p animated boolean Whether this emoji is animated.]=] +function get.animated(self) + return self._animated +end + +--[=[@p roles ArrayIterable An iterable array of roles that may be required to use this emoji, generally +related to integration-managed emojis. Object order is not guaranteed.]=] +function get.roles(self) + if not self._roles then + local roles = self._parent._roles + self._roles = ArrayIterable(self._roles_raw, function(id) + return roles:get(id) + end) + self._roles_raw = nil + end + return self._roles +end + +return Emoji diff --git a/deps/discordia/libs/containers/GroupChannel.lua b/deps/discordia/libs/containers/GroupChannel.lua new file mode 100644 index 0000000..4fc4ced --- /dev/null +++ b/deps/discordia/libs/containers/GroupChannel.lua @@ -0,0 +1,122 @@ +--[=[ +@c GroupChannel x TextChannel +@d Represents a Discord group channel. Essentially a private channel that may have +more than one and up to ten recipients. This class should only be relevant to +user-accounts; bots cannot normally join group channels. +]=] + +local json = require('json') + +local TextChannel = require('containers/abstract/TextChannel') +local SecondaryCache = require('iterables/SecondaryCache') +local Resolver = require('client/Resolver') + +local format = string.format + +local GroupChannel, get = require('class')('GroupChannel', TextChannel) + +function GroupChannel:__init(data, parent) + TextChannel.__init(self, data, parent) + self._recipients = SecondaryCache(data.recipients, self.client._users) +end + +--[=[ +@m setName +@t http +@p name string +@r boolean +@d Sets the channel's name. This must be between 1 and 100 characters in length. +]=] +function GroupChannel:setName(name) + return self:_modify({name = name or json.null}) +end + +--[=[ +@m setIcon +@t http +@p icon Base64-Resolvable +@r boolean +@d Sets the channel's icon. To remove the icon, pass `nil`. +]=] +function GroupChannel:setIcon(icon) + icon = icon and Resolver.base64(icon) + return self:_modify({icon = icon or json.null}) +end + +--[=[ +@m addRecipient +@t http +@p id User-ID-Resolvable +@r boolean +@d Adds a user to the channel. +]=] +function GroupChannel:addRecipient(id) + id = Resolver.userId(id) + local data, err = self.client._api:groupDMAddRecipient(self._id, id) + if data then + return true + else + return false, err + end +end + +--[=[ +@m removeRecipient +@t http +@p id User-ID-Resolvable +@r boolean +@d Removes a user from the channel. +]=] +function GroupChannel:removeRecipient(id) + id = Resolver.userId(id) + local data, err = self.client._api:groupDMRemoveRecipient(self._id, id) + if data then + return true + else + return false, err + end +end + +--[=[ +@m leave +@t http +@r boolean +@d Removes the client's user from the channel. If no users remain, the channel +is destroyed. +]=] +function GroupChannel:leave() + return self:_delete() +end + +--[=[@p recipients SecondaryCache A secondary cache of users that are present in the channel.]=] +function get.recipients(self) + return self._recipients +end + +--[=[@p name string The name of the channel.]=] +function get.name(self) + return self._name +end + +--[=[@p ownerId string The Snowflake ID of the user that owns (created) the channel.]=] +function get.ownerId(self) + return self._owner_id +end + +--[=[@p owner User/nil Equivalent to `GroupChannel.recipients:get(GroupChannel.ownerId)`.]=] +function get.owner(self) + return self._recipients:get(self._owner_id) +end + +--[=[@p icon string/nil The hash for the channel's custom icon, if one is set.]=] +function get.icon(self) + return self._icon +end + +--[=[@p iconURL string/nil The URL that can be used to view the channel's icon, if one is set.]=] +function get.iconURL(self) + local icon = self._icon + return icon and format('https://cdn.discordapp.com/channel-icons/%s/%s.png', self._id, icon) +end + +return GroupChannel diff --git a/deps/discordia/libs/containers/Guild.lua b/deps/discordia/libs/containers/Guild.lua new file mode 100644 index 0000000..51603f4 --- /dev/null +++ b/deps/discordia/libs/containers/Guild.lua @@ -0,0 +1,921 @@ +--[=[ +@c Guild x Snowflake +@d Represents a Discord guild (or server). Guilds are a collection of members, +channels, and roles that represents one community. +]=] + +local Cache = require('iterables/Cache') +local Role = require('containers/Role') +local Emoji = require('containers/Emoji') +local Invite = require('containers/Invite') +local Webhook = require('containers/Webhook') +local Ban = require('containers/Ban') +local Member = require('containers/Member') +local Resolver = require('client/Resolver') +local AuditLogEntry = require('containers/AuditLogEntry') +local GuildTextChannel = require('containers/GuildTextChannel') +local GuildVoiceChannel = require('containers/GuildVoiceChannel') +local GuildCategoryChannel = require('containers/GuildCategoryChannel') +local Snowflake = require('containers/abstract/Snowflake') + +local json = require('json') +local enums = require('enums') + +local channelType = enums.channelType +local floor = math.floor +local format = string.format + +local Guild, get = require('class')('Guild', Snowflake) + +function Guild:__init(data, parent) + Snowflake.__init(self, data, parent) + self._roles = Cache({}, Role, self) + self._emojis = Cache({}, Emoji, self) + self._members = Cache({}, Member, self) + self._text_channels = Cache({}, GuildTextChannel, self) + self._voice_channels = Cache({}, GuildVoiceChannel, self) + self._categories = Cache({}, GuildCategoryChannel, self) + self._voice_states = {} + if not data.unavailable then + return self:_makeAvailable(data) + end +end + +function Guild:_load(data) + Snowflake._load(self, data) + return self:_loadMore(data) +end + +function Guild:_loadMore(data) + if data.features then + self._features = data.features + end +end + +function Guild:_makeAvailable(data) + + self._roles:_load(data.roles) + self._emojis:_load(data.emojis) + self:_loadMore(data) + + if not data.channels then return end -- incomplete guild + + local states = self._voice_states + for _, state in ipairs(data.voice_states) do + states[state.user_id] = state + end + + local text_channels = self._text_channels + local voice_channels = self._voice_channels + local categories = self._categories + + for _, channel in ipairs(data.channels) do + local t = channel.type + if t == channelType.text or t == channelType.news then + text_channels:_insert(channel) + elseif t == channelType.voice then + voice_channels:_insert(channel) + elseif t == channelType.category then + categories:_insert(channel) + end + end + + return self:_loadMembers(data) + +end + +function Guild:_loadMembers(data) + local members = self._members + members:_load(data.members) + for _, presence in ipairs(data.presences) do + local member = members:get(presence.user.id) + if member then -- rogue presence check + member:_loadPresence(presence) + end + end + if self._large and self.client._options.cacheAllMembers then + return self:requestMembers() + end +end + +function Guild:_modify(payload) + local data, err = self.client._api:modifyGuild(self._id, payload) + if data then + self:_load(data) + return true + else + return false, err + end +end + +--[=[ +@m requestMembers +@t ws +@r boolean +@d Asynchronously loads all members for this guild. You do not need to call this +if the `cacheAllMembers` client option (and the `syncGuilds` option for +user-accounts) is enabled on start-up. +]=] +function Guild:requestMembers() + local shard = self.client._shards[self.shardId] + if not shard then + return false, 'Invalid shard' + end + if shard._loading then + shard._loading.chunks[self._id] = true + end + return shard:requestGuildMembers(self._id) +end + +--[=[ +@m sync +@t ws +@r boolean +@d Asynchronously loads certain data and enables the receiving of certain events +for this guild. You do not need to call this if the `syncGuilds` client option +is enabled on start-up. + +Note: This is only for user accounts. Bot accounts never need to sync guilds! +]=] +function Guild:sync() + local shard = self.client._shards[self.shardId] + if not shard then + return false, 'Invalid shard' + end + if shard._loading then + shard._loading.syncs[self._id] = true + end + return shard:syncGuilds({self._id}) +end + +--[=[ +@m getMember +@t http? +@p id User-ID-Resolvable +@r Member +@d Gets a member object by ID. If the object is already cached, then the cached +object will be returned; otherwise, an HTTP request is made. +]=] +function Guild:getMember(id) + id = Resolver.userId(id) + local member = self._members:get(id) + if member then + return member + else + local data, err = self.client._api:getGuildMember(self._id, id) + if data then + return self._members:_insert(data) + else + return nil, err + end + end +end + +--[=[ +@m getRole +@t mem +@p id Role-ID-Resolvable +@r Role +@d Gets a role object by ID. +]=] +function Guild:getRole(id) + id = Resolver.roleId(id) + return self._roles:get(id) +end + +--[=[ +@m getEmoji +@t mem +@p id Emoji-ID-Resolvable +@r Emoji +@d Gets a emoji object by ID. +]=] +function Guild:getEmoji(id) + id = Resolver.emojiId(id) + return self._emojis:get(id) +end + +--[=[ +@m getChannel +@t mem +@p id Channel-ID-Resolvable +@r GuildChannel +@d Gets a text, voice, or category channel object by ID. +]=] +function Guild:getChannel(id) + id = Resolver.channelId(id) + return self._text_channels:get(id) or self._voice_channels:get(id) or self._categories:get(id) +end + +--[=[ +@m createTextChannel +@t http +@p name string +@r GuildTextChannel +@d Creates a new text channel in this guild. The name must be between 2 and 100 +characters in length. +]=] +function Guild:createTextChannel(name) + local data, err = self.client._api:createGuildChannel(self._id, {name = name, type = channelType.text}) + if data then + return self._text_channels:_insert(data) + else + return nil, err + end +end + +--[=[ +@m createVoiceChannel +@t http +@p name string +@r GuildVoiceChannel +@d Creates a new voice channel in this guild. The name must be between 2 and 100 +characters in length. +]=] +function Guild:createVoiceChannel(name) + local data, err = self.client._api:createGuildChannel(self._id, {name = name, type = channelType.voice}) + if data then + return self._voice_channels:_insert(data) + else + return nil, err + end +end + +--[=[ +@m createCategory +@t http +@p name string +@r GuildCategoryChannel +@d Creates a channel category in this guild. The name must be between 2 and 100 +characters in length. +]=] +function Guild:createCategory(name) + local data, err = self.client._api:createGuildChannel(self._id, {name = name, type = channelType.category}) + if data then + return self._categories:_insert(data) + else + return nil, err + end +end + +--[=[ +@m createRole +@t http +@p name string +@r Role +@d Creates a new role in this guild. The name must be between 1 and 100 characters +in length. +]=] +function Guild:createRole(name) + local data, err = self.client._api:createGuildRole(self._id, {name = name}) + if data then + return self._roles:_insert(data) + else + return nil, err + end +end + +--[=[ +@m createEmoji +@t http +@p name string +@p image Base64-Resolvable +@r Emoji +@d Creates a new emoji in this guild. The name must be between 2 and 32 characters +in length. The image must not be over 256kb, any higher will return a 400 Bad Request +]=] +function Guild:createEmoji(name, image) + image = Resolver.base64(image) + local data, err = self.client._api:createGuildEmoji(self._id, {name = name, image = image}) + if data then + return self._emojis:_insert(data) + else + return nil, err + end +end + +--[=[ +@m setName +@t http +@p name string +@r boolean +@d Sets the guilds name. This must be between 2 and 100 characters in length. +]=] +function Guild:setName(name) + return self:_modify({name = name or json.null}) +end + +--[=[ +@m setRegion +@t http +@p region string +@r boolean +@d Sets the guild's voice region (eg: `us-east`). See `listVoiceRegions` for a list +of acceptable regions. +]=] +function Guild:setRegion(region) + return self:_modify({region = region or json.null}) +end + +--[=[ +@m setVerificationLevel +@t http +@p verification_level number +@r boolean +@d Sets the guild's verification level setting. See the `verificationLevel` +enumeration for acceptable values. +]=] +function Guild:setVerificationLevel(verification_level) + return self:_modify({verification_level = verification_level or json.null}) +end + +--[=[ +@m setNotificationSetting +@t http +@p default_message_notifications number +@r boolean +@d Sets the guild's default notification setting. See the `notficationSetting` +enumeration for acceptable values. +]=] +function Guild:setNotificationSetting(default_message_notifications) + return self:_modify({default_message_notifications = default_message_notifications or json.null}) +end + +--[=[ +@m setExplicitContentSetting +@t http +@p explicit_content_filter number +@r boolean +@d Sets the guild's explicit content level setting. See the `explicitContentLevel` +enumeration for acceptable values. +]=] +function Guild:setExplicitContentSetting(explicit_content_filter) + return self:_modify({explicit_content_filter = explicit_content_filter or json.null}) +end + +--[=[ +@m setAFKTimeout +@t http +@p afk_timeout number +@r number +@d Sets the guild's AFK timeout in seconds. +]=] +function Guild:setAFKTimeout(afk_timeout) + return self:_modify({afk_timeout = afk_timeout or json.null}) +end + +--[=[ +@m setAFKChannel +@t http +@p id Channel-ID-Resolvable +@r boolean +@d Sets the guild's AFK channel. +]=] +function Guild:setAFKChannel(id) + id = id and Resolver.channelId(id) + return self:_modify({afk_channel_id = id or json.null}) +end + +--[=[ +@m setSystemChannel +@t http +@p id Channel-Id-Resolvable +@r boolean +@d Sets the guild's join message channel. +]=] +function Guild:setSystemChannel(id) + id = id and Resolver.channelId(id) + return self:_modify({system_channel_id = id or json.null}) +end + +--[=[ +@m setOwner +@t http +@p id User-ID-Resolvable +@r boolean +@d Transfers ownership of the guild to another user. Only the current guild owner +can do this. +]=] +function Guild:setOwner(id) + id = id and Resolver.userId(id) + return self:_modify({owner_id = id or json.null}) +end + +--[=[ +@m setIcon +@t http +@p icon Base64-Resolvable +@r boolean +@d Sets the guild's icon. To remove the icon, pass `nil`. +]=] +function Guild:setIcon(icon) + icon = icon and Resolver.base64(icon) + return self:_modify({icon = icon or json.null}) +end + +--[=[ +@m setBanner +@t http +@p banner Base64-Resolvable +@r boolean +@d Sets the guild's banner. To remove the banner, pass `nil`. +]=] +function Guild:setBanner(banner) + banner = banner and Resolver.base64(banner) + return self:_modify({banner = banner or json.null}) +end + +--[=[ +@m setSplash +@t http +@p splash Base64-Resolvable +@r boolean +@d Sets the guild's splash. To remove the splash, pass `nil`. +]=] +function Guild:setSplash(splash) + splash = splash and Resolver.base64(splash) + return self:_modify({splash = splash or json.null}) +end + +--[=[ +@m getPruneCount +@t http +@op days number +@r number +@d Returns the number of members that would be pruned from the guild if a prune +were to be executed. +]=] +function Guild:getPruneCount(days) + local data, err = self.client._api:getGuildPruneCount(self._id, days and {days = days} or nil) + if data then + return data.pruned + else + return nil, err + end +end + +--[=[ +@m pruneMembers +@t http +@op days number +@op count boolean +@r number +@d Prunes (removes) inactive, roleless members from the guild who have not been online in the last provided days. +If the `count` boolean is provided, the number of pruned members is returned; otherwise, `0` is returned. +]=] +function Guild:pruneMembers(days, count) + local t1 = type(days) + if t1 == 'number' then + count = type(count) == 'boolean' and count + elseif t1 == 'boolean' then + count = days + days = nil + end + local data, err = self.client._api:beginGuildPrune(self._id, nil, { + days = days, + compute_prune_count = count, + }) + if data then + return data.pruned + else + return nil, err + end +end + +--[=[ +@m getBans +@t http +@r Cache +@d Returns a newly constructed cache of all ban objects for the guild. The +cache and its objects are not automatically updated via gateway events. You must +call this method again to get the updated objects. +]=] +function Guild:getBans() + local data, err = self.client._api:getGuildBans(self._id) + if data then + return Cache(data, Ban, self) + else + return nil, err + end +end + +--[=[ +@m getBan +@t http +@p id User-ID-Resolvable +@r Ban +@d This will return a Ban object for a giver user if that user is banned +from the guild; otherwise, `nil` is returned. +]=] +function Guild:getBan(id) + id = Resolver.userId(id) + local data, err = self.client._api:getGuildBan(self._id, id) + if data then + return Ban(data, self) + else + return nil, err + end +end + +--[=[ +@m getInvites +@t http +@r Cache +@d Returns a newly constructed cache of all invite objects for the guild. The +cache and its objects are not automatically updated via gateway events. You must +call this method again to get the updated objects. +]=] +function Guild:getInvites() + local data, err = self.client._api:getGuildInvites(self._id) + if data then + return Cache(data, Invite, self.client) + else + return nil, err + end +end + +--[=[ +@m getAuditLogs +@t http +@op query table +@r Cache +@d Returns a newly constructed cache of audit log entry objects for the guild. The +cache and its objects are not automatically updated via gateway events. You must +call this method again to get the updated objects. + +If included, the query parameters include: query.limit: number, query.user: UserId Resolvable +query.before: EntryId Resolvable, query.type: ActionType Resolvable +]=] +function Guild:getAuditLogs(query) + if type(query) == 'table' then + query = { + limit = query.limit, + user_id = Resolver.userId(query.user), + before = Resolver.entryId(query.before), + action_type = Resolver.actionType(query.type), + } + end + local data, err = self.client._api:getGuildAuditLog(self._id, query) + if data then + self.client._users:_load(data.users) + self.client._webhooks:_load(data.webhooks) + return Cache(data.audit_log_entries, AuditLogEntry, self) + else + return nil, err + end +end + +--[=[ +@m getWebhooks +@t http +@r Cache +@d Returns a newly constructed cache of all webhook objects for the guild. The +cache and its objects are not automatically updated via gateway events. You must +call this method again to get the updated objects. +]=] +function Guild:getWebhooks() + local data, err = self.client._api:getGuildWebhooks(self._id) + if data then + return Cache(data, Webhook, self.client) + else + return nil, err + end +end + +--[=[ +@m listVoiceRegions +@t http +@r table +@d Returns a raw data table that contains a list of available voice regions for +this guild, as provided by Discord, with no additional parsing. +]=] +function Guild:listVoiceRegions() + return self.client._api:getGuildVoiceRegions(self._id) +end + +--[=[ +@m leave +@t http +@r boolean +@d Removes the current user from the guild. +]=] +function Guild:leave() + local data, err = self.client._api:leaveGuild(self._id) + if data then + return true + else + return false, err + end +end + +--[=[ +@m delete +@t http +@r boolean +@d Permanently deletes the guild. The current user must owner the server. This cannot be undone! +]=] +function Guild:delete() + local data, err = self.client._api:deleteGuild(self._id) + if data then + local cache = self._parent._guilds + if cache then + cache:_delete(self._id) + end + return true + else + return false, err + end +end + +--[=[ +@m kickUser +@t http +@p id User-ID-Resolvable +@op reason string +@r boolean +@d Kicks a user/member from the guild with an optional reason. +]=] +function Guild:kickUser(id, reason) + id = Resolver.userId(id) + local query = reason and {reason = reason} + local data, err = self.client._api:removeGuildMember(self._id, id, query) + if data then + return true + else + return false, err + end +end + +--[=[ +@m banUser +@t http +@p id User-ID-Resolvable +@op reason string +@op days number +@r boolean +@d Bans a user/member from the guild with an optional reason. The `days` parameter +is the number of days to consider when purging messages, up to 7. +]=] +function Guild:banUser(id, reason, days) + local query = reason and {reason = reason} + if days then + query = query or {} + query['delete-message-days'] = days + end + id = Resolver.userId(id) + local data, err = self.client._api:createGuildBan(self._id, id, query) + if data then + return true + else + return false, err + end +end + +--[=[ +@m unbanUser +@t http +@p id User-ID-Resolvable +@op reason string +@r boolean +@d Unbans a user/member from the guild with an optional reason. +]=] +function Guild:unbanUser(id, reason) + id = Resolver.userId(id) + local query = reason and {reason = reason} + local data, err = self.client._api:removeGuildBan(self._id, id, query) + if data then + return true + else + return false, err + end +end + +--[=[@p shardId number The ID of the shard on which this guild is served. If only one shard is in +operation, then this will always be 0.]=] +function get.shardId(self) + return floor(self._id / 2^22) % self.client._total_shard_count +end + +--[=[@p name string The guild's name. This should be between 2 and 100 characters in length.]=] +function get.name(self) + return self._name +end + +--[=[@p icon string/nil The hash for the guild's custom icon, if one is set.]=] +function get.icon(self) + return self._icon +end + +--[=[@p iconURL string/nil The URL that can be used to view the guild's icon, if one is set.]=] +function get.iconURL(self) + local icon = self._icon + return icon and format('https://cdn.discordapp.com/icons/%s/%s.png', self._id, icon) +end + +--[=[@p splash string/nil The hash for the guild's custom splash image, if one is set. Only partnered +guilds may have this.]=] +function get.splash(self) + return self._splash +end + +--[=[@p splashURL string/nil The URL that can be used to view the guild's custom splash image, if one is set. +Only partnered guilds may have this.]=] +function get.splashURL(self) + local splash = self._splash + return splash and format('https://cdn.discordapp.com/splashes/%s/%s.png', self._id, splash) +end + +--[=[@p banner string/nil The hash for the guild's custom banner, if one is set.]=] +function get.banner(self) + return self._banner +end + +--[=[@p bannerURL string/nil The URL that can be used to view the guild's banner, if one is set.]=] +function get.bannerURL(self) + local banner = self._banner + return banner and format('https://cdn.discordapp.com/banners/%s/%s.png', self._id, banner) +end + +--[=[@p large boolean Whether the guild has an arbitrarily large amount of members. Guilds that are +"large" will not initialize with all members cached.]=] +function get.large(self) + return self._large +end + +--[=[@p lazy boolean Whether the guild follows rules for the lazy-loading of client data.]=] +function get.lazy(self) + return self._lazy +end + +--[=[@p region string The voice region that is used for all voice connections in the guild.]=] +function get.region(self) + return self._region +end + +--[=[@p vanityCode string/nil The guild's vanity invite URL code, if one exists.]=] +function get.vanityCode(self) + return self._vanity_url_code +end + +--[=[@p description string/nil The guild's custom description, if one exists.]=] +function get.description(self) + return self._description +end + +--[=[@p maxMembers number/nil The guild's maximum member count, if available.]=] +function get.maxMembers(self) + return self._max_members +end + +--[=[@p maxPresences number/nil The guild's maximum presence count, if available.]=] +function get.maxPresences(self) + return self._max_presences +end + +--[=[@p mfaLevel number The guild's multi-factor (or two-factor) verification level setting. A value of +0 indicates that MFA is not required; a value of 1 indicates that MFA is +required for administrative actions.]=] +function get.mfaLevel(self) + return self._mfa_level +end + +--[=[@p joinedAt string The date and time at which the current user joined the guild, represented as +an ISO 8601 string plus microseconds when available.]=] +function get.joinedAt(self) + return self._joined_at +end + +--[=[@p afkTimeout number The guild's voice AFK timeout in seconds.]=] +function get.afkTimeout(self) + return self._afk_timeout +end + +--[=[@p unavailable boolean Whether the guild is unavailable. If the guild is unavailable, then no property +is guaranteed to exist except for this one and the guild's ID.]=] +function get.unavailable(self) + return self._unavailable or false +end + +--[=[@p totalMemberCount number The total number of members that belong to this guild. This should always be +greater than or equal to the total number of cached members.]=] +function get.totalMemberCount(self) + return self._member_count +end + +--[=[@p verificationLevel number The guild's verification level setting. See the `verificationLevel` +enumeration for a human-readable representation.]=] +function get.verificationLevel(self) + return self._verification_level +end + +--[=[@p notificationSetting number The guild's default notification setting. See the `notficationSetting` +enumeration for a human-readable representation.]=] +function get.notificationSetting(self) + return self._default_message_notifications +end + +--[=[@p explicitContentSetting number The guild's explicit content level setting. See the `explicitContentLevel` +enumeration for a human-readable representation.]=] +function get.explicitContentSetting(self) + return self._explicit_content_filter +end + +--[=[@p premiumTier number The guild's premier tier affected by nitro server +boosts. See the `premiumTier` enumeration for a human-readable representation]=] +function get.premiumTier(self) + return self._premium_tier +end + +--[=[@p premiumSubscriptionCount number The number of users that have upgraded +the guild with nitro server boosting.]=] +function get.premiumSubscriptionCount(self) + return self._premium_subscription_count +end + +--[=[@p features table Raw table of VIP features that are enabled for the guild.]=] +function get.features(self) + return self._features +end + +--[=[@p me Member/nil Equivalent to `Guild.members:get(Guild.client.user.id)`.]=] +function get.me(self) + return self._members:get(self.client._user._id) +end + +--[=[@p owner Member/nil Equivalent to `Guild.members:get(Guild.ownerId)`.]=] +function get.owner(self) + return self._members:get(self._owner_id) +end + +--[=[@p ownerId string The Snowflake ID of the guild member that owns the guild.]=] +function get.ownerId(self) + return self._owner_id +end + +--[=[@p afkChannelId string/nil The Snowflake ID of the channel that is used for AFK members, if one is set.]=] +function get.afkChannelId(self) + return self._afk_channel_id +end + +--[=[@p afkChannel GuildVoiceChannel/nil Equivalent to `Guild.voiceChannels:get(Guild.afkChannelId)`.]=] +function get.afkChannel(self) + return self._voice_channels:get(self._afk_channel_id) +end + +--[=[@p systemChannelId string/nil The channel id where Discord's join messages will be displayed.]=] +function get.systemChannelId(self) + return self._system_channel_id +end + +--[=[@p systemChannel GuildTextChannel/nil The channel where Discord's join messages will be displayed.]=] +function get.systemChannel(self) + return self._text_channels:get(self._system_channel_id) +end + +--[=[@p defaultRole Role Equivalent to `Guild.roles:get(Guild.id)`.]=] +function get.defaultRole(self) + return self._roles:get(self._id) +end + +--[=[@p connection VoiceConnection/nil The VoiceConnection for this guild if one exists.]=] +function get.connection(self) + return self._connection +end + +--[=[@p roles Cache An iterable cache of all roles that exist in this guild. This includes the +default everyone role.]=] +function get.roles(self) + return self._roles +end + +--[=[@p emojis Cache An iterable cache of all emojis that exist in this guild. Note that standard +unicode emojis are not found here; only custom emojis.]=] +function get.emojis(self) + return self._emojis +end + +--[=[@p members Cache An iterable cache of all members that exist in this guild and have been +already loaded. If the `cacheAllMembers` client option (and the `syncGuilds` +option for user-accounts) is enabled on start-up, then all members will be +cached. Otherwise, offline members may not be cached. To access a member that +may exist, but is not cached, use `Guild:getMember`.]=] +function get.members(self) + return self._members +end + +--[=[@p textChannels Cache An iterable cache of all text channels that exist in this guild.]=] +function get.textChannels(self) + return self._text_channels +end + +--[=[@p voiceChannels Cache An iterable cache of all voice channels that exist in this guild.]=] +function get.voiceChannels(self) + return self._voice_channels +end + +--[=[@p categories Cache An iterable cache of all channel categories that exist in this guild.]=] +function get.categories(self) + return self._categories +end + +return Guild diff --git a/deps/discordia/libs/containers/GuildCategoryChannel.lua b/deps/discordia/libs/containers/GuildCategoryChannel.lua new file mode 100644 index 0000000..8dedd10 --- /dev/null +++ b/deps/discordia/libs/containers/GuildCategoryChannel.lua @@ -0,0 +1,83 @@ +--[=[ +@c GuildCategoryChannel x GuildChannel +@d Represents a channel category in a Discord guild, used to organize individual +text or voice channels in that guild. +]=] + +local GuildChannel = require('containers/abstract/GuildChannel') +local FilteredIterable = require('iterables/FilteredIterable') +local enums = require('enums') + +local channelType = enums.channelType + +local GuildCategoryChannel, get = require('class')('GuildCategoryChannel', GuildChannel) + +function GuildCategoryChannel:__init(data, parent) + GuildChannel.__init(self, data, parent) +end + +--[=[ +@m createTextChannel +@t http +@p name string +@r GuildTextChannel +@d Creates a new GuildTextChannel with this category as it's parent. Similar to `Guild:createTextChannel(name)` +]=] +function GuildCategoryChannel:createTextChannel(name) + local guild = self._parent + local data, err = guild.client._api:createGuildChannel(guild._id, { + name = name, + type = channelType.text, + parent_id = self._id + }) + if data then + return guild._text_channels:_insert(data) + else + return nil, err + end +end + +--[=[ +@m createVoiceChannel +@t http +@p name string +@r GuildVoiceChannel +@d Creates a new GuildVoiceChannel with this category as it's parent. Similar to `Guild:createVoiceChannel(name)` +]=] +function GuildCategoryChannel:createVoiceChannel(name) + local guild = self._parent + local data, err = guild.client._api:createGuildChannel(guild._id, { + name = name, + type = channelType.voice, + parent_id = self._id + }) + if data then + return guild._voice_channels:_insert(data) + else + return nil, err + end +end + +--[=[@p textChannels FilteredIterable Iterable of all textChannels in the Category.]=] +function get.textChannels(self) + if not self._text_channels then + local id = self._id + self._text_channels = FilteredIterable(self._parent._text_channels, function(c) + return c._parent_id == id + end) + end + return self._text_channels +end + +--[=[@p voiceChannels FilteredIterable Iterable of all voiceChannels in the Category.]=] +function get.voiceChannels(self) + if not self._voice_channels then + local id = self._id + self._voice_channels = FilteredIterable(self._parent._voice_channels, function(c) + return c._parent_id == id + end) + end + return self._voice_channels +end + +return GuildCategoryChannel diff --git a/deps/discordia/libs/containers/GuildTextChannel.lua b/deps/discordia/libs/containers/GuildTextChannel.lua new file mode 100644 index 0000000..388f57b --- /dev/null +++ b/deps/discordia/libs/containers/GuildTextChannel.lua @@ -0,0 +1,165 @@ +--[=[ +@c GuildTextChannel x GuildChannel x TextChannel +@d Represents a text channel in a Discord guild, where guild members and webhooks +can send and receive messages. +]=] + +local json = require('json') + +local GuildChannel = require('containers/abstract/GuildChannel') +local TextChannel = require('containers/abstract/TextChannel') +local FilteredIterable = require('iterables/FilteredIterable') +local Webhook = require('containers/Webhook') +local Cache = require('iterables/Cache') +local Resolver = require('client/Resolver') + +local GuildTextChannel, get = require('class')('GuildTextChannel', GuildChannel, TextChannel) + +function GuildTextChannel:__init(data, parent) + GuildChannel.__init(self, data, parent) + TextChannel.__init(self, data, parent) +end + +function GuildTextChannel:_load(data) + GuildChannel._load(self, data) + TextChannel._load(self, data) +end + +--[=[ +@m createWebhook +@t http +@p name string +@r Webhook +@d Creates a webhook for this channel. The name must be between 2 and 32 characters +in length. +]=] +function GuildTextChannel:createWebhook(name) + local data, err = self.client._api:createWebhook(self._id, {name = name}) + if data then + return Webhook(data, self.client) + else + return nil, err + end +end + +--[=[ +@m getWebhooks +@t http +@r Cache +@d Returns a newly constructed cache of all webhook objects for the channel. The +cache and its objects are not automatically updated via gateway events. You must +call this method again to get the updated objects. +]=] +function GuildTextChannel:getWebhooks() + local data, err = self.client._api:getChannelWebhooks(self._id) + if data then + return Cache(data, Webhook, self.client) + else + return nil, err + end +end + +--[=[ +@m bulkDelete +@t http +@p messages Message-ID-Resolvables +@r boolean +@d Bulk deletes multiple messages, from 2 to 100, from the channel. Messages over +2 weeks old cannot be deleted and will return an error. +]=] +function GuildTextChannel:bulkDelete(messages) + messages = Resolver.messageIds(messages) + local data, err + if #messages == 1 then + data, err = self.client._api:deleteMessage(self._id, messages[1]) + else + data, err = self.client._api:bulkDeleteMessages(self._id, {messages = messages}) + end + if data then + return true + else + return false, err + end +end + +--[=[ +@m setTopic +@t http +@p topic string +@r boolean +@d Sets the channel's topic. This must be between 1 and 1024 characters. Pass `nil` +to remove the topic. +]=] +function GuildTextChannel:setTopic(topic) + return self:_modify({topic = topic or json.null}) +end + +--[=[ +@m setRateLimit +@t http +@p limit number +@r boolean +@d Sets the channel's slowmode rate limit in seconds. This must be between 0 and 120. +Passing 0 or `nil` will clear the limit. +]=] +function GuildTextChannel:setRateLimit(limit) + return self:_modify({rate_limit_per_user = limit or json.null}) +end + +--[=[ +@m enableNSFW +@t http +@r boolean +@d Enables the NSFW setting for the channel. NSFW channels are hidden from users +until the user explicitly requests to view them. +]=] +function GuildTextChannel:enableNSFW() + return self:_modify({nsfw = true}) +end + +--[=[ +@m disableNSFW +@t http +@r boolean +@d Disables the NSFW setting for the channel. NSFW channels are hidden from users +until the user explicitly requests to view them. +]=] +function GuildTextChannel:disableNSFW() + return self:_modify({nsfw = false}) +end + +--[=[@p topic string/nil The channel's topic. This should be between 1 and 1024 characters.]=] +function get.topic(self) + return self._topic +end + +--[=[@p nsfw boolean Whether this channel is marked as NSFW (not safe for work).]=] +function get.nsfw(self) + return self._nsfw or false +end + +--[=[@p rateLimit number Slowmode rate limit per guild member.]=] +function get.rateLimit(self) + return self._rate_limit_per_user or 0 +end + +--[=[@p isNews boolean Whether this channel is a news channel of type 5.]=] +function get.isNews(self) + return self._type == 5 +end + +--[=[@p members FilteredIterable A filtered iterable of guild members that have +permission to read this channel. If you want to check whether a specific member +has permission to read this channel, it would be better to get the member object +elsewhere and use `Member:hasPermission` rather than check whether the member +exists here.]=] +function get.members(self) + if not self._members then + self._members = FilteredIterable(self._parent._members, function(m) + return m:hasPermission(self, 'readMessages') + end) + end + return self._members +end + +return GuildTextChannel diff --git a/deps/discordia/libs/containers/GuildVoiceChannel.lua b/deps/discordia/libs/containers/GuildVoiceChannel.lua new file mode 100644 index 0000000..d7ce63c --- /dev/null +++ b/deps/discordia/libs/containers/GuildVoiceChannel.lua @@ -0,0 +1,138 @@ +--[=[ +@c GuildVoiceChannel x GuildChannel +@d Represents a voice channel in a Discord guild, where guild members can connect +and communicate via voice chat. +]=] + +local json = require('json') + +local GuildChannel = require('containers/abstract/GuildChannel') +local VoiceConnection = require('voice/VoiceConnection') +local TableIterable = require('iterables/TableIterable') + +local GuildVoiceChannel, get = require('class')('GuildVoiceChannel', GuildChannel) + +function GuildVoiceChannel:__init(data, parent) + GuildChannel.__init(self, data, parent) +end + +--[=[ +@m setBitrate +@t http +@p bitrate number +@r boolean +@d Sets the channel's audio bitrate in bits per second (bps). This must be between +8000 and 96000 (or 128000 for partnered servers). If `nil` is passed, the +default is set, which is 64000. +]=] +function GuildVoiceChannel:setBitrate(bitrate) + return self:_modify({bitrate = bitrate or json.null}) +end + +--[=[ +@m setUserLimit +@t http +@p user_limit number +@r boolean +@d Sets the channel's user limit. This must be between 0 and 99 (where 0 is +unlimited). If `nil` is passed, the default is set, which is 0. +]=] +function GuildVoiceChannel:setUserLimit(user_limit) + return self:_modify({user_limit = user_limit or json.null}) +end + +--[=[ +@m join +@t ws +@r VoiceConnection +@d Join this channel and form a connection to the Voice Gateway. +]=] +function GuildVoiceChannel:join() + + local success, err + + local connection = self._connection + + if connection then + + if connection._ready then + return connection + end + + else + + local guild = self._parent + local client = guild._parent + + success, err = client._shards[guild.shardId]:updateVoice(guild._id, self._id) + + if not success then + return nil, err + end + + connection = guild._connection + + if not connection then + connection = VoiceConnection(self) + guild._connection = connection + end + + self._connection = connection + + end + + success, err = connection:_await() + + if success then + return connection + else + return nil, err + end + +end + +--[=[ +@m leave +@t http +@r boolean +@d Leave this channel if there is an existing voice connection to it. +Equivalent to GuildVoiceChannel.connection:close() +]=] +function GuildVoiceChannel:leave() + if self._connection then + return self._connection:close() + else + return false, 'No voice connection exists for this channel' + end +end + +--[=[@p bitrate number The channel's bitrate in bits per second (bps). This should be between 8000 and +96000 (or 128000 for partnered servers).]=] +function get.bitrate(self) + return self._bitrate +end + +--[=[@p userLimit number The amount of users allowed to be in this channel. +Users with `moveMembers` permission ignore this limit.]=] +function get.userLimit(self) + return self._user_limit +end + +--[=[@p connectedMembers TableIterable An iterable of all users connected to the channel.]=] +function get.connectedMembers(self) + if not self._connected_members then + local id = self._id + local members = self._parent._members + self._connected_members = TableIterable(self._parent._voice_states, function(state) + return state.channel_id == id and members:get(state.user_id) + end) + end + return self._connected_members +end + +--[=[@p connection VoiceConnection/nil The VoiceConnection for this channel if one exists.]=] +function get.connection(self) + return self._connection +end + +return GuildVoiceChannel diff --git a/deps/discordia/libs/containers/Invite.lua b/deps/discordia/libs/containers/Invite.lua new file mode 100644 index 0000000..6479892 --- /dev/null +++ b/deps/discordia/libs/containers/Invite.lua @@ -0,0 +1,187 @@ +--[=[ +@c Invite x Container +@d Represents an invitation to a Discord guild channel. Invites can be used to join +a guild, though they are not always permanent. +]=] + +local Container = require('containers/abstract/Container') +local json = require('json') + +local format = string.format +local null = json.null + +local function load(v) + return v ~= null and v or nil +end + +local Invite, get = require('class')('Invite', Container) + +function Invite:__init(data, parent) + Container.__init(self, data, parent) + self._guild_id = load(data.guild.id) + self._channel_id = load(data.channel.id) + self._guild_name = load(data.guild.name) + self._guild_icon = load(data.guild.icon) + self._guild_splash = load(data.guild.splash) + self._guild_banner = load(data.guild.banner) + self._guild_description = load(data.guild.description) + self._guild_verification_level = load(data.guild.verification_level) + self._channel_name = load(data.channel.name) + self._channel_type = load(data.channel.type) + if data.inviter then + self._inviter = self.client._users:_insert(data.inviter) + end +end + +--[=[ +@m __hash +@r string +@d Returns `Invite.code` +]=] +function Invite:__hash() + return self._code +end + +--[=[ +@m delete +@t http +@r boolean +@d Permanently deletes the invite. This cannot be undone! +]=] +function Invite:delete() + local data, err = self.client._api:deleteInvite(self._code) + if data then + return true + else + return false, err + end +end + +--[=[@p code string The invite's code which can be used to identify the invite.]=] +function get.code(self) + return self._code +end + +--[=[@p guildId string The Snowflake ID of the guild to which this invite belongs.]=] +function get.guildId(self) + return self._guild_id +end + +--[=[@p guildName string The name of the guild to which this invite belongs.]=] +function get.guildName(self) + return self._guild_name +end + +--[=[@p channelId string The Snowflake ID of the channel to which this belongs.]=] +function get.channelId(self) + return self._channel_id +end + +--[=[@p channelName string The name of the channel to which this invite belongs.]=] +function get.channelName(self) + return self._channel_name +end + +--[=[@p channelType number The type of the channel to which this invite belongs. Use the `channelType` +enumeration for a human-readable representation.]=] +function get.channelType(self) + return self._channel_type +end + +--[=[@p guildIcon string/nil The hash for the guild's custom icon, if one is set.]=] +function get.guildIcon(self) + return self._guild_icon +end + +--[=[@p guildBanner string/nil The hash for the guild's custom banner, if one is set.]=] +function get.guildBanner(self) + return self._guild_banner +end + +--[=[@p guildSplash string/nil The hash for the guild's custom splash, if one is set.]=] +function get.guildSplash(self) + return self._guild_splash +end + +--[=[@p guildIconURL string/nil The URL that can be used to view the guild's icon, if one is set.]=] +function get.guildIconURL(self) + local icon = self._guild_icon + return icon and format('https://cdn.discordapp.com/icons/%s/%s.png', self._guild_id, icon) or nil +end + +--[=[@p guildBannerURL string/nil The URL that can be used to view the guild's banner, if one is set.]=] +function get.guildBannerURL(self) + local banner = self._guild_banner + return banner and format('https://cdn.discordapp.com/banners/%s/%s.png', self._guild_id, banner) or nil +end + +--[=[@p guildSplashURL string/nil The URL that can be used to view the guild's splash, if one is set.]=] +function get.guildSplashURL(self) + local splash = self._guild_splash + return splash and format('https://cdn.discordapp.com/splashs/%s/%s.png', self._guild_id, splash) or nil +end + +--[=[@p guildDescription string/nil The guild's custom description, if one is set.]=] +function get.guildDescription(self) + return self._guild_description +end + +--[=[@p guildVerificationLevel number/nil The guild's verification level, if available.]=] +function get.guildVerificationLevel(self) + return self._guild_verification_level +end + +--[=[@p inviter User/nil The object of the user that created the invite. This will not exist if the +invite is a guild widget or a vanity invite.]=] +function get.inviter(self) + return self._inviter +end + +--[=[@p uses number/nil How many times this invite has been used. This will not exist if the invite is +accessed via `Client:getInvite`.]=] +function get.uses(self) + return self._uses +end + +--[=[@p maxUses number/nil The maximum amount of times this invite can be used. This will not exist if the +invite is accessed via `Client:getInvite`.]=] +function get.maxUses(self) + return self._max_uses +end + +--[=[@p maxAge number/nil How long, in seconds, this invite lasts before it expires. This will not exist +if the invite is accessed via `Client:getInvite`.]=] +function get.maxAge(self) + return self._max_age +end + +--[=[@p temporary boolean/nil Whether the invite grants temporary membership. This will not exist if the +invite is accessed via `Client:getInvite`.]=] +function get.temporary(self) + return self._temporary +end + +--[=[@p createdAt string/nil The date and time at which the invite was created, represented as an ISO 8601 +string plus microseconds when available. This will not exist if the invite is +accessed via `Client:getInvite`.]=] +function get.createdAt(self) + return self._created_at +end + +--[=[@p revoked boolean/nil Whether the invite has been revoked. This will not exist if the invite is +accessed via `Client:getInvite`.]=] +function get.revoked(self) + return self._revoked +end + +--[=[@p approximatePresenceCount number/nil The approximate count of online members.]=] +function get.approximatePresenceCount(self) + return self._approximate_presence_count +end + +--[=[@p approximateMemberCount number/nil The approximate count of all members.]=] +function get.approximateMemberCount(self) + return self._approximate_member_count +end + +return Invite diff --git a/deps/discordia/libs/containers/Member.lua b/deps/discordia/libs/containers/Member.lua new file mode 100644 index 0000000..1ca734c --- /dev/null +++ b/deps/discordia/libs/containers/Member.lua @@ -0,0 +1,539 @@ +--[=[ +@c Member x UserPresence +@d Represents a Discord guild member. Though one user may be a member in more than +one guild, each presence is represented by a different member object associated +with that guild. Note that any method or property that exists for the User class is +also available in the Member class. +]=] + +local enums = require('enums') +local class = require('class') +local UserPresence = require('containers/abstract/UserPresence') +local ArrayIterable = require('iterables/ArrayIterable') +local Color = require('utils/Color') +local Resolver = require('client/Resolver') +local GuildChannel = require('containers/abstract/GuildChannel') +local Permissions = require('utils/Permissions') + +local insert, remove, sort = table.insert, table.remove, table.sort +local band, bor, bnot = bit.band, bit.bor, bit.bnot +local isInstance = class.isInstance +local permission = enums.permission + +local Member, get = class('Member', UserPresence) + +function Member:__init(data, parent) + UserPresence.__init(self, data, parent) + return self:_loadMore(data) +end + +function Member:_load(data) + UserPresence._load(self, data) + return self:_loadMore(data) +end + +function Member:_loadMore(data) + if data.roles then + local roles = #data.roles > 0 and data.roles or nil + if self._roles then + self._roles._array = roles + else + self._roles_raw = roles + end + end +end + +local function sorter(a, b) + if a._position == b._position then + return tonumber(a._id) < tonumber(b._id) + else + return a._position > b._position + end +end + +local function predicate(role) + return role._color > 0 +end + +--[=[ +@m getColor +@t mem +@r Color +@d Returns a color object that represents the member's color as determined by +its highest colored role. If the member has no colored roles, then the default +color with a value of 0 is returned. +]=] +function Member:getColor() + local roles = {} + for role in self.roles:findAll(predicate) do + insert(roles, role) + end + sort(roles, sorter) + return roles[1] and roles[1]:getColor() or Color() +end + +local function has(a, b) + return band(a, b) > 0 +end + +--[=[ +@m hasPermission +@t mem +@op channel GuildChannel +@p perm Permissions-Resolvable +@r boolean +@d Checks whether the member has a specific permission. If `channel` is omitted, +then only guild-level permissions are checked. This is a relatively expensive +operation. If you need to check multiple permissions at once, use the +`getPermissions` method and check the resulting object. +]=] +function Member:hasPermission(channel, perm) + + if not perm then + perm = channel + channel = nil + end + + local guild = self.guild + if channel then + if not isInstance(channel, GuildChannel) or channel.guild ~= guild then + return error('Invalid GuildChannel: ' .. tostring(channel), 2) + end + end + + local n = Resolver.permission(perm) + if not n then + return error('Invalid permission: ' .. tostring(perm), 2) + end + + if self.id == guild.ownerId then + return true + end + + local rolePermissions = guild.defaultRole.permissions + + for role in self.roles:iter() do + if role.id ~= guild.id then -- just in case + rolePermissions = bor(rolePermissions, role.permissions) + end + end + + if has(rolePermissions, permission.administrator) then + return true + end + + if channel then + + local overwrites = channel.permissionOverwrites + + local overwrite = overwrites:get(self.id) + if overwrite then + if has(overwrite.allowedPermissions, n) then + return true + end + if has(overwrite.deniedPermissions, n) then + return false + end + end + + local allow, deny = 0, 0 + for role in self.roles:iter() do + if role.id ~= guild.id then -- just in case + overwrite = overwrites:get(role.id) + if overwrite then + allow = bor(allow, overwrite.allowedPermissions) + deny = bor(deny, overwrite.deniedPermissions) + end + end + end + + if has(allow, n) then + return true + end + if has(deny, n) then + return false + end + + local everyone = overwrites:get(guild.id) + if everyone then + if has(everyone.allowedPermissions, n) then + return true + end + if has(everyone.deniedPermissions, n) then + return false + end + end + + end + + return has(rolePermissions, n) + +end + +--[=[ +@m getPermissions +@t mem +@op channel GuildChannel +@r Permissions +@d Returns a permissions object that represents the member's total permissions for +the guild, or for a specific channel if one is provided. If you just need to +check one permission, use the `hasPermission` method. +]=] +function Member:getPermissions(channel) + + local guild = self.guild + if channel then + if not isInstance(channel, GuildChannel) or channel.guild ~= guild then + return error('Invalid GuildChannel: ' .. tostring(channel), 2) + end + end + + if self.id == guild.ownerId then + return Permissions.all() + end + + local ret = guild.defaultRole.permissions + + for role in self.roles:iter() do + if role.id ~= guild.id then -- just in case + ret = bor(ret, role.permissions) + end + end + + if band(ret, permission.administrator) > 0 then + return Permissions.all() + end + + if channel then + + local overwrites = channel.permissionOverwrites + + local everyone = overwrites:get(guild.id) + if everyone then + ret = band(ret, bnot(everyone.deniedPermissions)) + ret = bor(ret, everyone.allowedPermissions) + end + + local allow, deny = 0, 0 + for role in self.roles:iter() do + if role.id ~= guild.id then -- just in case + local overwrite = overwrites:get(role.id) + if overwrite then + deny = bor(deny, overwrite.deniedPermissions) + allow = bor(allow, overwrite.allowedPermissions) + end + end + end + ret = band(ret, bnot(deny)) + ret = bor(ret, allow) + + local overwrite = overwrites:get(self.id) + if overwrite then + ret = band(ret, bnot(overwrite.deniedPermissions)) + ret = bor(ret, overwrite.allowedPermissions) + end + + end + + return Permissions(ret) + +end + +--[=[ +@m addRole +@t http? +@p id Role-ID-Resolvable +@r boolean +@d Adds a role to the member. If the member already has the role, then no action is +taken. Note that the everyone role cannot be explicitly added. +]=] +function Member:addRole(id) + if self:hasRole(id) then return true end + id = Resolver.roleId(id) + local data, err = self.client._api:addGuildMemberRole(self._parent._id, self.id, id) + if data then + local roles = self._roles and self._roles._array or self._roles_raw + if roles then + insert(roles, id) + else + self._roles_raw = {id} + end + return true + else + return false, err + end +end + +--[=[ +@m removeRole +@t http? +@p id Role-ID-Resolvable +@r boolean +@d Removes a role from the member. If the member does not have the role, then no +action is taken. Note that the everyone role cannot be removed. +]=] +function Member:removeRole(id) + if not self:hasRole(id) then return true end + id = Resolver.roleId(id) + local data, err = self.client._api:removeGuildMemberRole(self._parent._id, self.id, id) + if data then + local roles = self._roles and self._roles._array or self._roles_raw + if roles then + for i, v in ipairs(roles) do + if v == id then + remove(roles, i) + break + end + end + if #roles == 0 then + if self._roles then + self._roles._array = nil + else + self._roles_raw = nil + end + end + end + return true + else + return false, err + end +end + +--[=[ +@m hasRole +@t mem +@p id Role-ID-Resolvable +@r boolean +@d Checks whether the member has a specific role. This will return true for the +guild's default role in addition to any explicitly assigned roles. +]=] +function Member:hasRole(id) + id = Resolver.roleId(id) + if id == self._parent._id then return true end -- @everyone + local roles = self._roles and self._roles._array or self._roles_raw + if roles then + for _, v in ipairs(roles) do + if v == id then + return true + end + end + end + return false +end + +--[=[ +@m setNickname +@t http +@p nick string +@r boolean +@d Sets the member's nickname. This must be between 1 and 32 characters in length. +Pass `nil` to remove the nickname. +]=] +function Member:setNickname(nick) + nick = nick or '' + local data, err + if self.id == self.client._user._id then + data, err = self.client._api:modifyCurrentUsersNick(self._parent._id, {nick = nick}) + else + data, err = self.client._api:modifyGuildMember(self._parent._id, self.id, {nick = nick}) + end + if data then + self._nick = nick ~= '' and nick or nil + return true + else + return false, err + end +end + +--[=[ +@m setVoiceChannel +@t http +@p id Channel-ID-Resolvable +@r boolean +@d Moves the member to a new voice channel, but only if the member has an active +voice connection in the current guild. Due to complexities in voice state +handling, the member's `voiceChannel` property will update asynchronously via +WebSocket; not as a result of the HTTP request. +]=] +function Member:setVoiceChannel(id) + id = Resolver.channelId(id) + local data, err = self.client._api:modifyGuildMember(self._parent._id, self.id, {channel_id = id}) + if data then + return true + else + return false, err + end +end + +--[=[ +@m mute +@t http +@r boolean +@d Mutes the member in its guild. +]=] +function Member:mute() + local data, err = self.client._api:modifyGuildMember(self._parent._id, self.id, {mute = true}) + if data then + self._mute = true + return true + else + return false, err + end +end + +--[=[ +@m unmute +@t http +@r boolean +@d Unmutes the member in its guild. +]=] +function Member:unmute() + local data, err = self.client._api:modifyGuildMember(self._parent._id, self.id, {mute = false}) + if data then + self._mute = false + return true + else + return false, err + end +end + +--[=[ +@m deafen +@t http +@r boolean +@d Deafens the member in its guild. +]=] +function Member:deafen() + local data, err = self.client._api:modifyGuildMember(self._parent._id, self.id, {deaf = true}) + if data then + self._deaf = true + return true + else + return false, err + end +end + +--[=[ +@m undeafen +@t http +@r boolean +@d Undeafens the member in its guild. +]=] +function Member:undeafen() + local data, err = self.client._api:modifyGuildMember(self._parent._id, self.id, {deaf = false}) + if data then + self._deaf = false + return true + else + return false, err + end +end + +--[=[ +@m kick +@t http +@p reason string +@r boolean +@d Equivalent to `Member.guild:kickUser(Member.user, reason)` +]=] +function Member:kick(reason) + return self._parent:kickUser(self._user, reason) +end + +--[=[ +@m ban +@t http +@p reason string +@p days number +@r boolean +@d Equivalent to `Member.guild:banUser(Member.user, reason, days)` +]=] +function Member:ban(reason, days) + return self._parent:banUser(self._user, reason, days) +end + +--[=[ +@m unban +@t http +@p reason string +@r boolean +@d Equivalent to `Member.guild:unbanUser(Member.user, reason)` +]=] +function Member:unban(reason) + return self._parent:unbanUser(self._user, reason) +end + +--[=[@p roles ArrayIterable An iterable array of guild roles that the member has. This does not explicitly +include the default everyone role. Object order is not guaranteed.]=] +function get.roles(self) + if not self._roles then + local roles = self._parent._roles + self._roles = ArrayIterable(self._roles_raw, function(id) + return roles:get(id) + end) + self._roles_raw = nil + end + return self._roles +end + +--[=[@p name string If the member has a nickname, then this will be equivalent to that nickname. +Otherwise, this is equivalent to `Member.user.username`.]=] +function get.name(self) + return self._nick or self._user._username +end + +--[=[@p nickname string/nil The member's nickname, if one is set.]=] +function get.nickname(self) + return self._nick +end + +--[=[@p joinedAt string/nil The date and time at which the current member joined the guild, represented as +an ISO 8601 string plus microseconds when available. Member objects generated +via presence updates lack this property.]=] +function get.joinedAt(self) + return self._joined_at +end + +--[=[@p premiumSince string/nil The date and time at which the current member boosted the guild, represented as +an ISO 8601 string plus microseconds when available.]=] +function get.premiumSince(self) + return self._premium_since +end + +--[=[@p voiceChannel GuildVoiceChannel/nil The voice channel to which this member is connected in the current guild.]=] +function get.voiceChannel(self) + local guild = self._parent + local state = guild._voice_states[self:__hash()] + return state and guild._voice_channels:get(state.channel_id) +end + +--[=[@p muted boolean Whether the member is voice muted in its guild.]=] +function get.muted(self) + local state = self._parent._voice_states[self:__hash()] + return state and (state.mute or state.self_mute) or self._mute +end + +--[=[@p deafened boolean Whether the member is voice deafened in its guild.]=] +function get.deafened(self) + local state = self._parent._voice_states[self:__hash()] + return state and (state.deaf or state.self_deaf) or self._deaf +end + +--[=[@p guild Guild The guild in which this member exists.]=] +function get.guild(self) + return self._parent +end + +--[=[@p highestRole Role The highest positioned role that the member has. If the member has no +explicit roles, then this is equivalent to `Member.guild.defaultRole`.]=] +function get.highestRole(self) + local ret + for role in self.roles:iter() do + if not ret or sorter(role, ret) then + ret = role + end + end + return ret or self.guild.defaultRole +end + +return Member diff --git a/deps/discordia/libs/containers/Message.lua b/deps/discordia/libs/containers/Message.lua new file mode 100644 index 0000000..acfdd28 --- /dev/null +++ b/deps/discordia/libs/containers/Message.lua @@ -0,0 +1,579 @@ +--[=[ +@c Message x Snowflake +@d Represents a text message sent in a Discord text channel. Messages can contain +simple content strings, rich embeds, attachments, or reactions. +]=] + +local json = require('json') +local enums = require('enums') +local constants = require('constants') +local Cache = require('iterables/Cache') +local ArrayIterable = require('iterables/ArrayIterable') +local Snowflake = require('containers/abstract/Snowflake') +local Reaction = require('containers/Reaction') +local Resolver = require('client/Resolver') + +local insert = table.insert +local null = json.null +local format = string.format +local messageFlag = enums.messageFlag +local band, bor, bnot = bit.band, bit.bor, bit.bnot + +local Message, get = require('class')('Message', Snowflake) + +function Message:__init(data, parent) + Snowflake.__init(self, data, parent) + self._author = self.client._users:_insert(data.author) + if data.member then + data.member.user = data.author + self._parent._parent._members:_insert(data.member) + end + self._timestamp = nil -- waste of space; can be calculated from Snowflake ID + if data.reactions and #data.reactions > 0 then + self._reactions = Cache(data.reactions, Reaction, self) + end + return self:_loadMore(data) +end + +function Message:_load(data) + Snowflake._load(self, data) + return self:_loadMore(data) +end + +local function parseMentions(content, pattern) + if not content:find('%b<>') then return end + local mentions, seen = {}, {} + for id in content:gmatch(pattern) do + if not seen[id] then + insert(mentions, id) + seen[id] = true + end + end + return mentions +end + +function Message:_loadMore(data) + + if data.mentions then + for _, user in ipairs(data.mentions) do + if user.member then + user.member.user = user + self._parent._parent._members:_insert(user.member) + else + self.client._users:_insert(user) + end + end + end + + local content = data.content + if content then + if self._mentioned_users then + self._mentioned_users._array = parseMentions(content, '<@!?(%d+)>') + end + if self._mentioned_roles then + self._mentioned_roles._array = parseMentions(content, '<@&(%d+)>') + end + if self._mentioned_channels then + self._mentioned_channels._array = parseMentions(content, '<#(%d+)>') + end + if self._mentioned_emojis then + self._mentioned_emojis._array = parseMentions(content, '') + end + self._clean_content = nil + end + + if data.embeds then + self._embeds = #data.embeds > 0 and data.embeds or nil + end + + if data.attachments then + self._attachments = #data.attachments > 0 and data.attachments or nil + end + +end + +function Message:_addReaction(d) + + local reactions = self._reactions + + if not reactions then + reactions = Cache({}, Reaction, self) + self._reactions = reactions + end + + local emoji = d.emoji + local k = emoji.id ~= null and emoji.id or emoji.name + local reaction = reactions:get(k) + + if reaction then + reaction._count = reaction._count + 1 + if d.user_id == self.client._user._id then + reaction._me = true + end + else + d.me = d.user_id == self.client._user._id + d.count = 1 + reaction = reactions:_insert(d) + end + return reaction + +end + +function Message:_removeReaction(d) + + local reactions = self._reactions + + local emoji = d.emoji + local k = emoji.id ~= null and emoji.id or emoji.name + local reaction = reactions:get(k) + + if not reaction then return nil end -- uncached reaction? + + reaction._count = reaction._count - 1 + if d.user_id == self.client._user._id then + reaction._me = false + end + + if reaction._count == 0 then + reactions:_delete(k) + end + + return reaction + +end + +function Message:_setOldContent(d) + local ts = d.edited_timestamp + if not ts then return end + local old = self._old + if old then + old[ts] = old[ts] or self._content + else + self._old = {[ts] = self._content} + end +end + +function Message:_modify(payload) + local data, err = self.client._api:editMessage(self._parent._id, self._id, payload) + if data then + self:_setOldContent(data) + self:_load(data) + return true + else + return false, err + end +end + +--[=[ +@m setContent +@t http +@p content string +@r boolean +@d Sets the message's content. The message must be authored by the current user +(ie: you cannot change the content of messages sent by other users). The content +must be from 1 to 2000 characters in length. +]=] +function Message:setContent(content) + return self:_modify({content = content or null}) +end + +--[=[ +@m setEmbed +@t http +@p embed table +@r boolean +@d Sets the message's embed. The message must be authored by the current user. +(ie: you cannot change the embed of messages sent by other users). +]=] +function Message:setEmbed(embed) + return self:_modify({embed = embed or null}) +end + +--[=[ +@m hideEmbeds +@t http +@r boolean +@d Hides all embeds for this message. +]=] +function Message:hideEmbeds() + local flags = bor(self._flags or 0, messageFlag.suppressEmbeds) + return self:_modify({flags = flags}) +end + +--[=[ +@m showEmbeds +@t http +@r boolean +@d Shows all embeds for this message. +]=] +function Message:showEmbeds() + local flags = band(self._flags or 0, bnot(messageFlag.suppressEmbeds)) + return self:_modify({flags = flags}) +end + +--[=[ +@m hasFlag +@t mem +@p flag Message-Flag-Resolvable +@r boolean +@d Indicates whether the message has a particular flag set. +]=] +function Message:hasFlag(flag) + flag = Resolver.messageFlag(flag) + return band(self._flags or 0, flag) > 0 +end + +--[=[ +@m update +@t http +@p data table +@r boolean +@d Sets multiple properties of the message at the same time using a table similar +to the one supported by `TextChannel.send`, except only `content` and `embed` +are valid fields; `mention(s)`, `file(s)`, etc are not supported. The message +must be authored by the current user. (ie: you cannot change the embed of messages +sent by other users). +]=] +function Message:update(data) + return self:_modify({ + content = data.content or null, + embed = data.embed or null, + }) +end + +--[=[ +@m pin +@t http +@r boolean +@d Pins the message in the channel. +]=] +function Message:pin() + local data, err = self.client._api:addPinnedChannelMessage(self._parent._id, self._id) + if data then + self._pinned = true + return true + else + return false, err + end +end + +--[=[ +@m unpin +@t http +@r boolean +@d Unpins the message in the channel. +]=] +function Message:unpin() + local data, err = self.client._api:deletePinnedChannelMessage(self._parent._id, self._id) + if data then + self._pinned = false + return true + else + return false, err + end +end + +--[=[ +@m addReaction +@t http +@p emoji Emoji-Resolvable +@r boolean +@d Adds a reaction to the message. Note that this does not return the new reaction +object; wait for the `reactionAdd` event instead. +]=] +function Message:addReaction(emoji) + emoji = Resolver.emoji(emoji) + local data, err = self.client._api:createReaction(self._parent._id, self._id, emoji) + if data then + return true + else + return false, err + end +end + +--[=[ +@m removeReaction +@t http +@p emoji Emoji-Resolvable +@op id User-ID-Resolvable +@r boolean +@d Removes a reaction from the message. Note that this does not return the old +reaction object; wait for the `reactionRemove` event instead. If no user is +indicated, then this will remove the current user's reaction. +]=] +function Message:removeReaction(emoji, id) + emoji = Resolver.emoji(emoji) + local data, err + if id then + id = Resolver.userId(id) + data, err = self.client._api:deleteUserReaction(self._parent._id, self._id, emoji, id) + else + data, err = self.client._api:deleteOwnReaction(self._parent._id, self._id, emoji) + end + if data then + return true + else + return false, err + end +end + +--[=[ +@m clearReactions +@t http +@r boolean +@d Removes all reactions from the message. +]=] +function Message:clearReactions() + local data, err = self.client._api:deleteAllReactions(self._parent._id, self._id) + if data then + return true + else + return false, err + end +end + +--[=[ +@m delete +@t http +@r boolean +@d Permanently deletes the message. This cannot be undone! +]=] +function Message:delete() + local data, err = self.client._api:deleteMessage(self._parent._id, self._id) + if data then + local cache = self._parent._messages + if cache then + cache:_delete(self._id) + end + return true + else + return false, err + end +end + +--[=[ +@m reply +@t http +@p content string/table +@r Message +@d Equivalent to `Message.channel:send(content)`. +]=] +function Message:reply(content) + return self._parent:send(content) +end + +--[=[@p reactions Cache An iterable cache of all reactions that exist for this message.]=] +function get.reactions(self) + if not self._reactions then + self._reactions = Cache({}, Reaction, self) + end + return self._reactions +end + +--[=[@p mentionedUsers ArrayIterable An iterable array of all users that are mentioned in this message.]=] +function get.mentionedUsers(self) + if not self._mentioned_users then + local users = self.client._users + local mentions = parseMentions(self._content, '<@!?(%d+)>') + self._mentioned_users = ArrayIterable(mentions, function(id) + return users:get(id) + end) + end + return self._mentioned_users +end + +--[=[@p mentionedRoles ArrayIterable An iterable array of known roles that are mentioned in this message, excluding +the default everyone role. The message must be in a guild text channel and the +roles must be cached in that channel's guild for them to appear here.]=] +function get.mentionedRoles(self) + if not self._mentioned_roles then + local client = self.client + local mentions = parseMentions(self._content, '<@&(%d+)>') + self._mentioned_roles = ArrayIterable(mentions, function(id) + local guild = client._role_map[id] + return guild and guild._roles:get(id) or nil + end) + end + return self._mentioned_roles +end + +--[=[@p mentionedEmojis ArrayIterable An iterable array of all known emojis that are mentioned in this message. If +the client does not have the emoji cached, then it will not appear here.]=] +function get.mentionedEmojis(self) + if not self._mentioned_emojis then + local client = self.client + local mentions = parseMentions(self._content, '') + self._mentioned_emojis = ArrayIterable(mentions, function(id) + local guild = client._emoji_map[id] + return guild and guild._emojis:get(id) + end) + end + return self._mentioned_emojis +end + +--[=[@p mentionedChannels ArrayIterable An iterable array of all known channels that are mentioned in this message. If +the client does not have the channel cached, then it will not appear here.]=] +function get.mentionedChannels(self) + if not self._mentioned_channels then + local client = self.client + local mentions = parseMentions(self._content, '<#(%d+)>') + self._mentioned_channels = ArrayIterable(mentions, function(id) + local guild = client._channel_map[id] + if guild then + return guild._text_channels:get(id) or guild._voice_channels:get(id) or guild._categories:get(id) + else + return client._private_channels:get(id) or client._group_channels:get(id) + end + end) + end + return self._mentioned_channels +end + +local usersMeta = {__index = function(_, k) return '@' .. k end} +local rolesMeta = {__index = function(_, k) return '@' .. k end} +local channelsMeta = {__index = function(_, k) return '#' .. k end} +local everyone = '@' .. constants.ZWSP .. 'everyone' +local here = '@' .. constants.ZWSP .. 'here' + +--[=[@p cleanContent string The message content with all recognized mentions replaced by names and with +@everyone and @here mentions escaped by a zero-width space (ZWSP).]=] +function get.cleanContent(self) + + if not self._clean_content then + + local content = self._content + local guild = self.guild + + local users = setmetatable({}, usersMeta) + for user in self.mentionedUsers:iter() do + local member = guild and guild._members:get(user._id) + users[user._id] = '@' .. (member and member._nick or user._username) + end + + local roles = setmetatable({}, rolesMeta) + for role in self.mentionedRoles:iter() do + roles[role._id] = '@' .. role._name + end + + local channels = setmetatable({}, channelsMeta) + for channel in self.mentionedChannels:iter() do + channels[channel._id] = '#' .. channel._name + end + + self._clean_content = content + :gsub('<@!?(%d+)>', users) + :gsub('<@&(%d+)>', roles) + :gsub('<#(%d+)>', channels) + :gsub('', '%1') + :gsub('@everyone', everyone) + :gsub('@here', here) + + end + + return self._clean_content + +end + +--[=[@p mentionsEveryone boolean Whether this message mentions @everyone or @here.]=] +function get.mentionsEveryone(self) + return self._mention_everyone +end + +--[=[@p pinned boolean Whether this message belongs to its channel's pinned messages.]=] +function get.pinned(self) + return self._pinned +end + +--[=[@p tts boolean Whether this message is a text-to-speech message.]=] +function get.tts(self) + return self._tts +end + +--[=[@p nonce string/number/boolean/nil Used by the official Discord client to detect the success of a sent message.]=] +function get.nonce(self) + return self._nonce +end + +--[=[@p editedTimestamp string/nil The date and time at which the message was most recently edited, represented as +an ISO 8601 string plus microseconds when available.]=] +function get.editedTimestamp(self) + return self._edited_timestamp +end + +--[=[@p oldContent string/table Yields a table containing keys as timestamps and +value as content of the message at that time.]=] +function get.oldContent(self) + return self._old +end + +--[=[@p content string The raw message content. This should be between 0 and 2000 characters in length.]=] +function get.content(self) + return self._content +end + +--[=[@p author User The object of the user that created the message.]=] +function get.author(self) + return self._author +end + +--[=[@p channel TextChannel The channel in which this message was sent.]=] +function get.channel(self) + return self._parent +end + +--[=[@p type number The message type. Use the `messageType` enumeration for a human-readable +representation.]=] +function get.type(self) + return self._type +end + +--[=[@p embed table/nil A raw data table that represents the first rich embed that exists in this +message. See the Discord documentation for more information.]=] +function get.embed(self) + return self._embeds and self._embeds[1] +end + +--[=[@p attachment table/nil A raw data table that represents the first file attachment that exists in this +message. See the Discord documentation for more information.]=] +function get.attachment(self) + return self._attachments and self._attachments[1] +end + +--[=[@p embeds table A raw data table that contains all embeds that exist for this message. If +there are none, this table will not be present.]=] +function get.embeds(self) + return self._embeds +end + +--[=[@p attachments table A raw data table that contains all attachments that exist for this message. If +there are none, this table will not be present.]=] +function get.attachments(self) + return self._attachments +end + +--[=[@p guild Guild/nil The guild in which this message was sent. This will not exist if the message +was not sent in a guild text channel. Equivalent to `Message.channel.guild`.]=] +function get.guild(self) + return self._parent.guild +end + +--[=[@p member Member/nil The member object of the message's author. This will not exist if the message +is not sent in a guild text channel or if the member object is not cached. +Equivalent to `Message.guild.members:get(Message.author.id)`.]=] +function get.member(self) + local guild = self.guild + return guild and guild._members:get(self._author._id) +end + +--[=[@p link string URL that can be used to jump-to the message in the Discord client.]=] +function get.link(self) + local guild = self.guild + return format('https://discord.com/channels/%s/%s/%s', guild and guild._id or '@me', self._parent._id, self._id) +end + +--[=[@p webhookId string/nil The ID of the webhook that generated this message, if applicable.]=] +function get.webhookId(self) + return self._webhook_id +end + +return Message diff --git a/deps/discordia/libs/containers/PermissionOverwrite.lua b/deps/discordia/libs/containers/PermissionOverwrite.lua new file mode 100644 index 0000000..294ab97 --- /dev/null +++ b/deps/discordia/libs/containers/PermissionOverwrite.lua @@ -0,0 +1,234 @@ +--[=[ +@c PermissionOverwrite x Snowflake +@d Represents an object that is used to allow or deny specific permissions for a +role or member in a Discord guild channel. +]=] + +local Snowflake = require('containers/abstract/Snowflake') +local Permissions = require('utils/Permissions') +local Resolver = require('client/Resolver') + +local band, bnot = bit.band, bit.bnot + +local PermissionOverwrite, get = require('class')('PermissionOverwrite', Snowflake) + +function PermissionOverwrite:__init(data, parent) + Snowflake.__init(self, data, parent) +end + +--[=[ +@m delete +@t http +@r boolean +@d Deletes the permission overwrite. This can be undone by creating a new version of +the same overwrite. +]=] +function PermissionOverwrite:delete() + local data, err = self.client._api:deleteChannelPermission(self._parent._id, self._id) + if data then + local cache = self._parent._permission_overwrites + if cache then + cache:_delete(self._id) + end + return true + else + return false, err + end +end + +--[=[ +@m getObject +@t http? +@r Role/Member +@d Returns the object associated with this overwrite, either a role or member. +This may make an HTTP request if the object is not cached. +]=] +function PermissionOverwrite:getObject() + local guild = self._parent._parent + if self._type == 'role' then + return guild:getRole(self._id) + elseif self._type == 'member' then + return guild:getMember(self._id) + end +end + +local function getPermissions(self) + return Permissions(self._allow), Permissions(self._deny) +end + +local function setPermissions(self, allow, deny) + local data, err = self.client._api:editChannelPermissions(self._parent._id, self._id, { + allow = allow, deny = deny, type = self._type + }) + if data then + self._allow, self._deny = allow, deny + return true + else + return false, err + end +end + +--[=[ +@m getAllowedPermissions +@t mem +@r Permissions +@d Returns a permissions object that represents the permissions that this overwrite +explicitly allows. +]=] +function PermissionOverwrite:getAllowedPermissions() + return Permissions(self._allow) +end + +--[=[ +@m getDeniedPermissions +@t mem +@r Permissions +@d Returns a permissions object that represents the permissions that this overwrite +explicitly denies. +]=] +function PermissionOverwrite:getDeniedPermissions() + return Permissions(self._deny) +end + +--[=[ +@m setPermissions +@t http +@p allowed Permissions-Resolvables +@p denied Permissions-Resolvables +@r boolean +@d Sets the permissions that this overwrite explicitly allows and denies. This +method does NOT resolve conflicts. Please be sure to use the correct parameters. +]=] +function PermissionOverwrite:setPermissions(allowed, denied) + local allow = Resolver.permissions(allowed) + local deny = Resolver.permissions(denied) + return setPermissions(self, allow, deny) +end + +--[=[ +@m setAllowedPermissions +@t http +@p allowed Permissions-Resolvables +@r boolean +@d Sets the permissions that this overwrite explicitly allows. +]=] +function PermissionOverwrite:setAllowedPermissions(allowed) + local allow = Resolver.permissions(allowed) + local deny = band(bnot(allow), self._deny) -- un-deny the allowed permissions + return setPermissions(self, allow, deny) +end + +--[=[ +@m setDeniedPermissions +@t http +@p denied Permissions-Resolvables +@r boolean +@d Sets the permissions that this overwrite explicitly denies. +]=] +function PermissionOverwrite:setDeniedPermissions(denied) + local deny = Resolver.permissions(denied) + local allow = band(bnot(deny), self._allow) -- un-allow the denied permissions + return setPermissions(self, allow, deny) +end + +--[=[ +@m allowPermissions +@t http +@p ... Permission-Resolvables +@r boolean +@d Allows individual permissions in this overwrite. +]=] +function PermissionOverwrite:allowPermissions(...) + local allowed, denied = getPermissions(self) + allowed:enable(...); denied:disable(...) + return setPermissions(self, allowed._value, denied._value) +end + +--[=[ +@m denyPermissions +@t http +@p ... Permission-Resolvables +@r boolean +@d Denies individual permissions in this overwrite. +]=] +function PermissionOverwrite:denyPermissions(...) + local allowed, denied = getPermissions(self) + allowed:disable(...); denied:enable(...) + return setPermissions(self, allowed._value, denied._value) +end + +--[=[ +@m clearPermissions +@t http +@p ... Permission-Resolvables +@r boolean +@d Clears individual permissions in this overwrite. +]=] +function PermissionOverwrite:clearPermissions(...) + local allowed, denied = getPermissions(self) + allowed:disable(...); denied:disable(...) + return setPermissions(self, allowed._value, denied._value) +end + +--[=[ +@m allowAllPermissions +@t http +@r boolean +@d Allows all permissions in this overwrite. +]=] +function PermissionOverwrite:allowAllPermissions() + local allowed, denied = getPermissions(self) + allowed:enableAll(); denied:disableAll() + return setPermissions(self, allowed._value, denied._value) +end + +--[=[ +@m denyAllPermissions +@t http +@r boolean +@d Denies all permissions in this overwrite. +]=] +function PermissionOverwrite:denyAllPermissions() + local allowed, denied = getPermissions(self) + allowed:disableAll(); denied:enableAll() + return setPermissions(self, allowed._value, denied._value) +end + +--[=[ +@m clearAllPermissions +@t http +@r boolean +@d Clears all permissions in this overwrite. +]=] +function PermissionOverwrite:clearAllPermissions() + local allowed, denied = getPermissions(self) + allowed:disableAll(); denied:disableAll() + return setPermissions(self, allowed._value, denied._value) +end + +--[=[@p type string The overwrite type; either "role" or "member".]=] +function get.type(self) + return self._type +end + +--[=[@p channel GuildChannel The channel in which this overwrite exists.]=] +function get.channel(self) + return self._parent +end + +--[=[@p guild Guild The guild in which this overwrite exists. Equivalent to `PermissionOverwrite.channel.guild`.]=] +function get.guild(self) + return self._parent._parent +end + +--[=[@p allowedPermissions number The number representing the total permissions allowed by this overwrite.]=] +function get.allowedPermissions(self) + return self._allow +end + +--[=[@p deniedPermissions number The number representing the total permissions denied by this overwrite.]=] +function get.deniedPermissions(self) + return self._deny +end + +return PermissionOverwrite diff --git a/deps/discordia/libs/containers/PrivateChannel.lua b/deps/discordia/libs/containers/PrivateChannel.lua new file mode 100644 index 0000000..2ab1471 --- /dev/null +++ b/deps/discordia/libs/containers/PrivateChannel.lua @@ -0,0 +1,37 @@ +--[=[ +@c PrivateChannel x TextChannel +@d Represents a private Discord text channel used to track correspondences between +the current user and one other recipient. +]=] + +local TextChannel = require('containers/abstract/TextChannel') + +local PrivateChannel, get = require('class')('PrivateChannel', TextChannel) + +function PrivateChannel:__init(data, parent) + TextChannel.__init(self, data, parent) + self._recipient = self.client._users:_insert(data.recipients[1]) +end + +--[=[ +@m close +@t http +@r boolean +@d Closes the channel. This does not delete the channel. To re-open the channel, +use `User:getPrivateChannel`. +]=] +function PrivateChannel:close() + return self:_delete() +end + +--[=[@p name string Equivalent to `PrivateChannel.recipient.username`.]=] +function get.name(self) + return self._recipient._username +end + +--[=[@p recipient User The recipient of this channel's messages, other than the current user.]=] +function get.recipient(self) + return self._recipient +end + +return PrivateChannel diff --git a/deps/discordia/libs/containers/Reaction.lua b/deps/discordia/libs/containers/Reaction.lua new file mode 100644 index 0000000..9270b0f --- /dev/null +++ b/deps/discordia/libs/containers/Reaction.lua @@ -0,0 +1,149 @@ +--[=[ +@c Reaction x Container +@d Represents an emoji that has been used to react to a Discord text message. Both +standard and custom emojis can be used. +]=] + +local json = require('json') +local Container = require('containers/abstract/Container') +local SecondaryCache = require('iterables/SecondaryCache') +local Resolver = require('client/Resolver') + +local null = json.null +local format = string.format + +local Reaction, get = require('class')('Reaction', Container) + +function Reaction:__init(data, parent) + Container.__init(self, data, parent) + local emoji = data.emoji + self._emoji_id = emoji.id ~= null and emoji.id or nil + self._emoji_name = emoji.name + if emoji.animated ~= null and emoji.animated ~= nil then -- not always present + self._emoji_animated = emoji.animated + end +end + +--[=[ +@m __hash +@r string +@d Returns `Reaction.emojiId or Reaction.emojiName` +]=] +function Reaction:__hash() + return self._emoji_id or self._emoji_name +end + +local function getUsers(self, query) + local emoji = Resolver.emoji(self) + local message = self._parent + local channel = message._parent + local data, err = self.client._api:getReactions(channel._id, message._id, emoji, query) + if data then + return SecondaryCache(data, self.client._users) + else + return nil, err + end +end + +--[=[ +@m getUsers +@t http +@op limit number +@r SecondaryCache +@d Returns a newly constructed cache of all users that have used this reaction in +its parent message. The cache is not automatically updated via gateway events, +but the internally referenced user objects may be updated. You must call this +method again to guarantee that the objects are update to date. +]=] +function Reaction:getUsers(limit) + return getUsers(self, limit and {limit = limit}) +end + +--[=[ +@m getUsersBefore +@t http +@p id User-ID-Resolvable +@op limit number +@r SecondaryCache +@d Returns a newly constructed cache of all users that have used this reaction before the specified id in +its parent message. The cache is not automatically updated via gateway events, +but the internally referenced user objects may be updated. You must call this +method again to guarantee that the objects are update to date. +]=] +function Reaction:getUsersBefore(id, limit) + id = Resolver.userId(id) + return getUsers(self, {before = id, limit = limit}) +end + +--[=[ +@m getUsersAfter +@t http +@p id User-ID-Resolvable +@op limit number +@r SecondaryCache +@d Returns a newly constructed cache of all users that have used this reaction +after the specified id in its parent message. The cache is not automatically +updated via gateway events, but the internally referenced user objects may be +updated. You must call this method again to guarantee that the objects are update to date. +]=] +function Reaction:getUsersAfter(id, limit) + id = Resolver.userId(id) + return getUsers(self, {after = id, limit = limit}) +end + +--[=[ +@m delete +@t http +@op id User-ID-Resolvable +@r boolean +@d Equivalent to `Reaction.message:removeReaction(Reaction)` +]=] +function Reaction:delete(id) + return self._parent:removeReaction(self, id) +end + +--[=[@p emojiId string/nil The ID of the emoji used in this reaction if it is a custom emoji.]=] +function get.emojiId(self) + return self._emoji_id +end + +--[=[@p emojiName string The name of the emoji used in this reaction. +This will be the raw string for a standard emoji.]=] +function get.emojiName(self) + return self._emoji_name +end + +--[=[@p emojiHash string The discord hash for the emoji used in this reaction. +This will be the raw string for a standard emoji.]=] +function get.emojiHash(self) + if self._emoji_id then + return self._emoji_name .. ':' .. self._emoji_id + else + return self._emoji_name + end +end + +--[=[@p emojiURL string/nil string The URL that can be used to view a full +version of the emoji used in this reaction if it is a custom emoji.]=] +function get.emojiURL(self) + local id = self._emoji_id + local ext = self._emoji_animated and 'gif' or 'png' + return id and format('https://cdn.discordapp.com/emojis/%s.%s', id, ext) or nil +end + +--[=[@p me boolean Whether the current user has used this reaction.]=] +function get.me(self) + return self._me +end + +--[=[@p count number The total number of users that have used this reaction.]=] +function get.count(self) + return self._count +end + +--[=[@p message Message The message on which this reaction exists.]=] +function get.message(self) + return self._parent +end + +return Reaction diff --git a/deps/discordia/libs/containers/Relationship.lua b/deps/discordia/libs/containers/Relationship.lua new file mode 100644 index 0000000..d7acc7a --- /dev/null +++ b/deps/discordia/libs/containers/Relationship.lua @@ -0,0 +1,27 @@ +--[=[ +@c Relationship x UserPresence +@d Represents a relationship between the current user and another Discord user. +This is generally either a friend or a blocked user. This class should only be +relevant to user-accounts; bots cannot normally have relationships. +]=] + +local UserPresence = require('containers/abstract/UserPresence') + +local Relationship, get = require('class')('Relationship', UserPresence) + +function Relationship:__init(data, parent) + UserPresence.__init(self, data, parent) +end + +--[=[@p name string Equivalent to `Relationship.user.username`.]=] +function get.name(self) + return self._user._username +end + +--[=[@p type number The relationship type. See the `relationshipType` enumeration for a +human-readable representation.]=] +function get.type(self) + return self._type +end + +return Relationship diff --git a/deps/discordia/libs/containers/Role.lua b/deps/discordia/libs/containers/Role.lua new file mode 100644 index 0000000..4d35dcb --- /dev/null +++ b/deps/discordia/libs/containers/Role.lua @@ -0,0 +1,383 @@ +--[=[ +@c Role x Snowflake +@d Represents a Discord guild role, which is used to assign priority, permissions, +and a color to guild members. +]=] + +local json = require('json') +local Snowflake = require('containers/abstract/Snowflake') +local Color = require('utils/Color') +local Permissions = require('utils/Permissions') +local Resolver = require('client/Resolver') +local FilteredIterable = require('iterables/FilteredIterable') + +local format = string.format +local insert, sort = table.insert, table.sort +local min, max, floor = math.min, math.max, math.floor +local huge = math.huge + +local Role, get = require('class')('Role', Snowflake) + +function Role:__init(data, parent) + Snowflake.__init(self, data, parent) + self.client._role_map[self._id] = parent +end + +function Role:_modify(payload) + local data, err = self.client._api:modifyGuildRole(self._parent._id, self._id, payload) + if data then + self:_load(data) + return true + else + return false, err + end +end + +--[=[ +@m delete +@t http +@r boolean +@d Permanently deletes the role. This cannot be undone! +]=] +function Role:delete() + local data, err = self.client._api:deleteGuildRole(self._parent._id, self._id) + if data then + local cache = self._parent._roles + if cache then + cache:_delete(self._id) + end + return true + else + return false, err + end +end + +local function sorter(a, b) + if a.position == b.position then + return tonumber(a.id) < tonumber(b.id) + else + return a.position < b.position + end +end + +local function getSortedRoles(self) + local guild = self._parent + local id = self._parent._id + local ret = {} + for role in guild.roles:iter() do + if role._id ~= id then + insert(ret, {id = role._id, position = role._position}) + end + end + sort(ret, sorter) + return ret +end + +local function setSortedRoles(self, roles) + local id = self._parent._id + insert(roles, {id = id, position = 0}) + local data, err = self.client._api:modifyGuildRolePositions(id, roles) + if data then + return true + else + return false, err + end +end + +--[=[ +@m moveDown +@t http +@p n number +@r boolean +@d Moves a role down its list. The parameter `n` indicates how many spaces the +role should be moved, clamped to the lowest position, with a default of 1 if +it is omitted. This will also normalize the positions of all roles. Note that +the default everyone role cannot be moved. +]=] +function Role:moveDown(n) -- TODO: fix attempt to move roles that cannot be moved + + n = tonumber(n) or 1 + if n < 0 then + return self:moveDown(-n) + end + + local roles = getSortedRoles(self) + + local new = huge + for i = #roles, 1, -1 do + local v = roles[i] + if v.id == self._id then + new = max(1, i - floor(n)) + v.position = new + elseif i >= new then + v.position = i + 1 + else + v.position = i + end + end + + return setSortedRoles(self, roles) + +end + +--[=[ +@m moveUp +@t http +@p n number +@r boolean +@d Moves a role up its list. The parameter `n` indicates how many spaces the +role should be moved, clamped to the highest position, with a default of 1 if +it is omitted. This will also normalize the positions of all roles. Note that +the default everyone role cannot be moved. +]=] +function Role:moveUp(n) -- TODO: fix attempt to move roles that cannot be moved + + n = tonumber(n) or 1 + if n < 0 then + return self:moveUp(-n) + end + + local roles = getSortedRoles(self) + + local new = -huge + for i = 1, #roles do + local v = roles[i] + if v.id == self._id then + new = min(i + floor(n), #roles) + v.position = new + elseif i <= new then + v.position = i - 1 + else + v.position = i + end + end + + return setSortedRoles(self, roles) + +end + +--[=[ +@m setName +@t http +@p name string +@r boolean +@d Sets the role's name. The name must be between 1 and 100 characters in length. +]=] +function Role:setName(name) + return self:_modify({name = name or json.null}) +end + +--[=[ +@m setColor +@t http +@p color Color-Resolvable +@r boolean +@d Sets the role's display color. +]=] +function Role:setColor(color) + color = color and Resolver.color(color) + return self:_modify({color = color or json.null}) +end + +--[=[ +@m setPermissions +@t http +@p permissions Permissions-Resolvable +@r boolean +@d Sets the permissions that this role explicitly allows. +]=] +function Role:setPermissions(permissions) + permissions = permissions and Resolver.permissions(permissions) + return self:_modify({permissions = permissions or json.null}) +end + +--[=[ +@m hoist +@t http +@r boolean +@d Causes members with this role to display above unhoisted roles in the member +list. +]=] +function Role:hoist() + return self:_modify({hoist = true}) +end + +--[=[ +@m unhoist +@t http +@r boolean +@d Causes member with this role to display amongst other unhoisted members. +]=] +function Role:unhoist() + return self:_modify({hoist = false}) +end + +--[=[ +@m enableMentioning +@t http +@r boolean +@d Allows anyone to mention this role in text messages. +]=] +function Role:enableMentioning() + return self:_modify({mentionable = true}) +end + +--[=[ +@m disableMentioning +@t http +@r boolean +@d Disallows anyone to mention this role in text messages. +]=] +function Role:disableMentioning() + return self:_modify({mentionable = false}) +end + +--[=[ +@m enablePermissions +@t http +@p ... Permission-Resolvables +@r boolean +@d Enables individual permissions for this role. This does not necessarily fully +allow the permissions. +]=] +function Role:enablePermissions(...) + local permissions = self:getPermissions() + permissions:enable(...) + return self:setPermissions(permissions) +end + +--[=[ +@m disablePermissions +@t http +@p ... Permission-Resolvables +@r boolean +@d Disables individual permissions for this role. This does not necessarily fully +disallow the permissions. +]=] +function Role:disablePermissions(...) + local permissions = self:getPermissions() + permissions:disable(...) + return self:setPermissions(permissions) +end + +--[=[ +@m enableAllPermissions +@t http +@r boolean +@d Enables all permissions for this role. This does not necessarily fully +allow the permissions. +]=] +function Role:enableAllPermissions() + local permissions = self:getPermissions() + permissions:enableAll() + return self:setPermissions(permissions) +end + +--[=[ +@m disableAllPermissions +@t http +@r boolean +@d Disables all permissions for this role. This does not necessarily fully +disallow the permissions. +]=] +function Role:disableAllPermissions() + local permissions = self:getPermissions() + permissions:disableAll() + return self:setPermissions(permissions) +end + +--[=[ +@m getColor +@t mem +@r Color +@d Returns a color object that represents the role's display color. +]=] +function Role:getColor() + return Color(self._color) +end + +--[=[ +@m getPermissions +@t mem +@r Permissions +@d Returns a permissions object that represents the permissions that this role +has enabled. +]=] +function Role:getPermissions() + return Permissions(self._permissions) +end + +--[=[@p hoisted boolean Whether members with this role should be shown separated from other members +in the guild member list.]=] +function get.hoisted(self) + return self._hoist +end + +--[=[@p mentionable boolean Whether this role can be mentioned in a text channel message.]=] +function get.mentionable(self) + return self._mentionable +end + +--[=[@p managed boolean Whether this role is managed by some integration or bot inclusion.]=] +function get.managed(self) + return self._managed +end + +--[=[@p name string The name of the role. This should be between 1 and 100 characters in length.]=] +function get.name(self) + return self._name +end + +--[=[@p position number The position of the role, where 0 is the lowest.]=] +function get.position(self) + return self._position +end + +--[=[@p color number Represents the display color of the role as a decimal value.]=] +function get.color(self) + return self._color +end + +--[=[@p permissions number Represents the total permissions of the role as a decimal value.]=] +function get.permissions(self) + return self._permissions +end + +--[=[@p mentionString string A string that, when included in a message content, may resolve as a role +notification in the official Discord client.]=] +function get.mentionString(self) + return format('<@&%s>', self._id) +end + +--[=[@p guild Guild The guild in which this role exists.]=] +function get.guild(self) + return self._parent +end + +--[=[@p members FilteredIterable A filtered iterable of guild members that have +this role. If you want to check whether a specific member has this role, it would +be better to get the member object elsewhere and use `Member:hasRole` rather +than check whether the member exists here.]=] +function get.members(self) + if not self._members then + self._members = FilteredIterable(self._parent._members, function(m) + return m:hasRole(self) + end) + end + return self._members +end + +--[=[@p emojis FilteredIterable A filtered iterable of guild emojis that have +this role. If you want to check whether a specific emoji has this role, it would +be better to get the emoji object elsewhere and use `Emoji:hasRole` rather +than check whether the emoji exists here.]=] +function get.emojis(self) + if not self._emojis then + self._emojis = FilteredIterable(self._parent._emojis, function(e) + return e:hasRole(self) + end) + end + return self._emojis +end + +return Role diff --git a/deps/discordia/libs/containers/User.lua b/deps/discordia/libs/containers/User.lua new file mode 100644 index 0000000..81d5509 --- /dev/null +++ b/deps/discordia/libs/containers/User.lua @@ -0,0 +1,186 @@ +--[=[ +@c User x Snowflake +@d Represents a single user of Discord, either a human or a bot, outside of any +specific guild's context. +]=] + +local Snowflake = require('containers/abstract/Snowflake') +local FilteredIterable = require('iterables/FilteredIterable') +local constants = require('constants') + +local format = string.format +local DEFAULT_AVATARS = constants.DEFAULT_AVATARS + +local User, get = require('class')('User', Snowflake) + +function User:__init(data, parent) + Snowflake.__init(self, data, parent) +end + +--[=[ +@m getAvatarURL +@t mem +@op size number +@op ext string +@r string +@d Returns a URL that can be used to view the user's full avatar. If provided, the +size must be a power of 2 while the extension must be a valid image format. If +the user does not have a custom avatar, the default URL is returned. +]=] +function User:getAvatarURL(size, ext) + local avatar = self._avatar + if avatar then + ext = ext or avatar:find('a_') == 1 and 'gif' or 'png' + if size then + return format('https://cdn.discordapp.com/avatars/%s/%s.%s?size=%s', self._id, avatar, ext, size) + else + return format('https://cdn.discordapp.com/avatars/%s/%s.%s', self._id, avatar, ext) + end + else + return self:getDefaultAvatarURL(size) + end +end + +--[=[ +@m getDefaultAvatarURL +@t mem +@op size number +@r string +@d Returns a URL that can be used to view the user's default avatar. +]=] +function User:getDefaultAvatarURL(size) + local avatar = self.defaultAvatar + if size then + return format('https://cdn.discordapp.com/embed/avatars/%s.png?size=%s', avatar, size) + else + return format('https://cdn.discordapp.com/embed/avatars/%s.png', avatar) + end +end + +--[=[ +@m getPrivateChannel +@t http +@r PrivateChannel +@d Returns a private channel that can be used to communicate with the user. If the +channel is not cached an HTTP request is made to open one. +]=] +function User:getPrivateChannel() + local id = self._id + local client = self.client + local channel = client._private_channels:find(function(e) return e._recipient._id == id end) + if channel then + return channel + else + local data, err = client._api:createDM({recipient_id = id}) + if data then + return client._private_channels:_insert(data) + else + return nil, err + end + end +end + +--[=[ +@m send +@t http +@p content string/table +@r Message +@d Equivalent to `User:getPrivateChannel():send(content)` +]=] +function User:send(content) + local channel, err = self:getPrivateChannel() + if channel then + return channel:send(content) + else + return nil, err + end +end + +--[=[ +@m sendf +@t http +@p content string +@r Message +@d Equivalent to `User:getPrivateChannel():sendf(content)` +]=] +function User:sendf(content, ...) + local channel, err = self:getPrivateChannel() + if channel then + return channel:sendf(content, ...) + else + return nil, err + end +end + +--[=[@p bot boolean Whether this user is a bot.]=] +function get.bot(self) + return self._bot or false +end + +--[=[@p name string Equivalent to `User.username`.]=] +function get.name(self) + return self._username +end + +--[=[@p username string The name of the user. This should be between 2 and 32 characters in length.]=] +function get.username(self) + return self._username +end + +--[=[@p discriminator number The discriminator of the user. This is a 4-digit string that is used to +discriminate the user from other users with the same username.]=] +function get.discriminator(self) + return self._discriminator +end + +--[=[@p tag string The user's username and discriminator concatenated by an `#`.]=] +function get.tag(self) + return self._username .. '#' .. self._discriminator +end + +function get.fullname(self) + self.client:_deprecated(self.__name, 'fullname', 'tag') + return self._username .. '#' .. self._discriminator +end + +--[=[@p avatar string/nil The hash for the user's custom avatar, if one is set.]=] +function get.avatar(self) + return self._avatar +end + +--[=[@p defaultAvatar number The user's default avatar. See the `defaultAvatar` enumeration for a +human-readable representation.]=] +function get.defaultAvatar(self) + return self._discriminator % DEFAULT_AVATARS +end + +--[=[@p avatarURL string Equivalent to the result of calling `User:getAvatarURL()`.]=] +function get.avatarURL(self) + return self:getAvatarURL() +end + +--[=[@p defaultAvatarURL string Equivalent to the result of calling `User:getDefaultAvatarURL()`.]=] +function get.defaultAvatarURL(self) + return self:getDefaultAvatarURL() +end + +--[=[@p mentionString string A string that, when included in a message content, may resolve as user +notification in the official Discord client.]=] +function get.mentionString(self) + return format('<@%s>', self._id) +end + +--[=[@p mutualGuilds FilteredIterable A iterable cache of all guilds where this user shares a membership with the +current user. The guild must be cached on the current client and the user's +member object must be cached in that guild in order for it to appear here.]=] +function get.mutualGuilds(self) + if not self._mutual_guilds then + local id = self._id + self._mutual_guilds = FilteredIterable(self.client._guilds, function(g) + return g._members:get(id) + end) + end + return self._mutual_guilds +end + +return User diff --git a/deps/discordia/libs/containers/Webhook.lua b/deps/discordia/libs/containers/Webhook.lua new file mode 100644 index 0000000..610af95 --- /dev/null +++ b/deps/discordia/libs/containers/Webhook.lua @@ -0,0 +1,147 @@ +--[=[ +@c Webhook x Snowflake +@d Represents a handle used to send webhook messages to a guild text channel in a +one-way fashion. This class defines methods and properties for managing the +webhook, not for sending messages. +]=] + +local json = require('json') +local enums = require('enums') +local Snowflake = require('containers/abstract/Snowflake') +local User = require('containers/User') +local Resolver = require('client/Resolver') + +local defaultAvatar = enums.defaultAvatar + +local Webhook, get = require('class')('Webhook', Snowflake) + +function Webhook:__init(data, parent) + Snowflake.__init(self, data, parent) + self._user = data.user and self.client._users:_insert(data.user) -- DNE if getting by token +end + +function Webhook:_modify(payload) + local data, err = self.client._api:modifyWebhook(self._id, payload) + if data then + self:_load(data) + return true + else + return false, err + end +end + +--[=[ +@m getAvatarURL +@t mem +@op size number +@op ext string +@r string +@d Returns a URL that can be used to view the webhooks's full avatar. If provided, +the size must be a power of 2 while the extension must be a valid image format. +If the webhook does not have a custom avatar, the default URL is returned. +]=] +function Webhook:getAvatarURL(size, ext) + return User.getAvatarURL(self, size, ext) +end + +--[=[ +@m getDefaultAvatarURL +@t mem +@op size number +@r string +@d Returns a URL that can be used to view the webhooks's default avatar. +]=] +function Webhook:getDefaultAvatarURL(size) + return User.getDefaultAvatarURL(self, size) +end + +--[=[ +@m setName +@t http +@p name string +@r boolean +@d Sets the webhook's name. This must be between 2 and 32 characters in length. +]=] +function Webhook:setName(name) + return self:_modify({name = name or json.null}) +end + +--[=[ +@m setAvatar +@t http +@p avatar Base64-Resolvable +@r boolean +@d Sets the webhook's avatar. If `nil` is passed, the avatar is removed. +]=] +function Webhook:setAvatar(avatar) + avatar = avatar and Resolver.base64(avatar) + return self:_modify({avatar = avatar or json.null}) +end + +--[=[ +@m delete +@t http +@r boolean +@d Permanently deletes the webhook. This cannot be undone! +]=] +function Webhook:delete() + local data, err = self.client._api:deleteWebhook(self._id) + if data then + return true + else + return false, err + end +end + +--[=[@p guildId string The ID of the guild in which this webhook exists.]=] +function get.guildId(self) + return self._guild_id +end + +--[=[@p channelId string The ID of the channel in which this webhook exists.]=] +function get.channelId(self) + return self._channel_id +end + +--[=[@p user User/nil The user that created this webhook.]=] +function get.user(self) + return self._user +end + +--[=[@p token string The token that can be used to access this webhook.]=] +function get.token(self) + return self._token +end + +--[=[@p name string The name of the webhook. This should be between 2 and 32 characters in length.]=] +function get.name(self) + return self._name +end + +--[=[@p type number The type of the webhook. See the `webhookType` enum for a human-readable representation.]=] +function get.type(self) + return self._type +end + +--[=[@p avatar string/nil The hash for the webhook's custom avatar, if one is set.]=] +function get.avatar(self) + return self._avatar +end + +--[=[@p avatarURL string Equivalent to the result of calling `Webhook:getAvatarURL()`.]=] +function get.avatarURL(self) + return self:getAvatarURL() +end + +--[=[@p defaultAvatar number The default avatar for the webhook. See the `defaultAvatar` enumeration for +a human-readable representation. This should always be `defaultAvatar.blurple`.]=] +function get.defaultAvatar() + return defaultAvatar.blurple +end + +--[=[@p defaultAvatarURL string Equivalent to the result of calling `Webhook:getDefaultAvatarURL()`.]=] +function get.defaultAvatarURL(self) + return self:getDefaultAvatarURL() +end + +return Webhook diff --git a/deps/discordia/libs/containers/abstract/Channel.lua b/deps/discordia/libs/containers/abstract/Channel.lua new file mode 100644 index 0000000..d8f4d1a --- /dev/null +++ b/deps/discordia/libs/containers/abstract/Channel.lua @@ -0,0 +1,66 @@ +--[=[ +@c Channel x Snowflake +@t abc +@d Defines the base methods and properties for all Discord channel types. +]=] + +local Snowflake = require('containers/abstract/Snowflake') +local enums = require('enums') + +local format = string.format +local channelType = enums.channelType + +local Channel, get = require('class')('Channel', Snowflake) + +function Channel:__init(data, parent) + Snowflake.__init(self, data, parent) +end + +function Channel:_modify(payload) + local data, err = self.client._api:modifyChannel(self._id, payload) + if data then + self:_load(data) + return true + else + return false, err + end +end + +function Channel:_delete() + local data, err = self.client._api:deleteChannel(self._id) + if data then + local cache + local t = self._type + if t == channelType.text or t == channelType.news then + cache = self._parent._text_channels + elseif t == channelType.private then + cache = self._parent._private_channels + elseif t == channelType.group then + cache = self._parent._group_channels + elseif t == channelType.voice then + cache = self._parent._voice_channels + elseif t == channelType.category then + cache = self._parent._categories + end + if cache then + cache:_delete(self._id) + end + return true + else + return false, err + end +end + +--[=[@p type number The channel type. See the `channelType` enumeration for a +human-readable representation.]=] +function get.type(self) + return self._type +end + +--[=[@p mentionString string A string that, when included in a message content, +may resolve as a link to a channel in the official Discord client.]=] +function get.mentionString(self) + return format('<#%s>', self._id) +end + +return Channel diff --git a/deps/discordia/libs/containers/abstract/Container.lua b/deps/discordia/libs/containers/abstract/Container.lua new file mode 100644 index 0000000..7826267 --- /dev/null +++ b/deps/discordia/libs/containers/abstract/Container.lua @@ -0,0 +1,68 @@ +--[=[ +@c Container +@t abc +@d Defines the base methods and properties for all Discord objects and +structures. Container classes are constructed internally with information +received from Discord and should never be manually constructed. +]=] + +local json = require('json') + +local null = json.null +local format = string.format + +local Container, get = require('class')('Container') + +local types = {['string'] = true, ['number'] = true, ['boolean'] = true} + +local function load(self, data) + -- assert(type(data) == 'table') -- debug + for k, v in pairs(data) do + if types[type(v)] then + self['_' .. k] = v + elseif v == null then + self['_' .. k] = nil + end + end +end + +function Container:__init(data, parent) + -- assert(type(parent) == 'table') -- debug + self._parent = parent + return load(self, data) +end + +--[=[ +@m __eq +@r boolean +@d Defines the behavior of the `==` operator. Allows containers to be directly +compared according to their type and `__hash` return values. +]=] +function Container:__eq(other) + return self.__class == other.__class and self:__hash() == other:__hash() +end + +--[=[ +@m __tostring +@r string +@d Defines the behavior of the `tostring` function. All containers follow the format +`ClassName: hash`. +]=] +function Container:__tostring() + return format('%s: %s', self.__name, self:__hash()) +end + +Container._load = load + +--[=[@p client Client A shortcut to the client object to which this container is visible.]=] +function get.client(self) + return self._parent.client or self._parent +end + +--[=[@p parent Container/Client The parent object of to which this container is +a child. For example, the parent of a role is the guild in which the role exists.]=] +function get.parent(self) + return self._parent +end + +return Container diff --git a/deps/discordia/libs/containers/abstract/GuildChannel.lua b/deps/discordia/libs/containers/abstract/GuildChannel.lua new file mode 100644 index 0000000..3fa4b5f --- /dev/null +++ b/deps/discordia/libs/containers/abstract/GuildChannel.lua @@ -0,0 +1,281 @@ +--[=[ +@c GuildChannel x Channel +@t abc +@d Defines the base methods and properties for all Discord guild channels. +]=] + +local json = require('json') +local enums = require('enums') +local class = require('class') +local Channel = require('containers/abstract/Channel') +local PermissionOverwrite = require('containers/PermissionOverwrite') +local Invite = require('containers/Invite') +local Cache = require('iterables/Cache') +local Resolver = require('client/Resolver') + +local isInstance = class.isInstance +local classes = class.classes +local channelType = enums.channelType + +local insert, sort = table.insert, table.sort +local min, max, floor = math.min, math.max, math.floor +local huge = math.huge + +local GuildChannel, get = class('GuildChannel', Channel) + +function GuildChannel:__init(data, parent) + Channel.__init(self, data, parent) + self.client._channel_map[self._id] = parent + self._permission_overwrites = Cache({}, PermissionOverwrite, self) + return self:_loadMore(data) +end + +function GuildChannel:_load(data) + Channel._load(self, data) + return self:_loadMore(data) +end + +function GuildChannel:_loadMore(data) + return self._permission_overwrites:_load(data.permission_overwrites, true) +end + +--[=[ +@m setName +@t http +@p name string +@r boolean +@d Sets the channel's name. This must be between 2 and 100 characters in length. +]=] +function GuildChannel:setName(name) + return self:_modify({name = name or json.null}) +end + +--[=[ +@m setCategory +@t http +@p id Channel-ID-Resolvable +@r boolean +@d Sets the channel's parent category. +]=] +function GuildChannel:setCategory(id) + id = Resolver.channelId(id) + return self:_modify({parent_id = id or json.null}) +end + +local function sorter(a, b) + if a.position == b.position then + return tonumber(a.id) < tonumber(b.id) + else + return a.position < b.position + end +end + +local function getSortedChannels(self) + + local channels + local t = self._type + if t == channelType.text or t == channelType.news then + channels = self._parent._text_channels + elseif t == channelType.voice then + channels = self._parent._voice_channels + elseif t == channelType.category then + channels = self._parent._categories + end + + local ret = {} + for channel in channels:iter() do + insert(ret, {id = channel._id, position = channel._position}) + end + sort(ret, sorter) + + return ret + +end + +local function setSortedChannels(self, channels) + local data, err = self.client._api:modifyGuildChannelPositions(self._parent._id, channels) + if data then + return true + else + return false, err + end +end + +--[=[ +@m moveUp +@t http +@p n number +@r boolean +@d Moves a channel up its list. The parameter `n` indicates how many spaces the +channel should be moved, clamped to the highest position, with a default of 1 if +it is omitted. This will also normalize the positions of all channels. +]=] +function GuildChannel:moveUp(n) + + n = tonumber(n) or 1 + if n < 0 then + return self:moveDown(-n) + end + + local channels = getSortedChannels(self) + + local new = huge + for i = #channels - 1, 0, -1 do + local v = channels[i + 1] + if v.id == self._id then + new = max(0, i - floor(n)) + v.position = new + elseif i >= new then + v.position = i + 1 + else + v.position = i + end + end + + return setSortedChannels(self, channels) + +end + +--[=[ +@m moveDown +@t http +@p n number +@r boolean +@d Moves a channel down its list. The parameter `n` indicates how many spaces the +channel should be moved, clamped to the lowest position, with a default of 1 if +it is omitted. This will also normalize the positions of all channels. +]=] +function GuildChannel:moveDown(n) + + n = tonumber(n) or 1 + if n < 0 then + return self:moveUp(-n) + end + + local channels = getSortedChannels(self) + + local new = -huge + for i = 0, #channels - 1 do + local v = channels[i + 1] + if v.id == self._id then + new = min(i + floor(n), #channels - 1) + v.position = new + elseif i <= new then + v.position = i - 1 + else + v.position = i + end + end + + return setSortedChannels(self, channels) + +end + +--[=[ +@m createInvite +@t http +@op payload table +@r Invite +@d Creates an invite to the channel. Optional payload fields are: max_age: number +time in seconds until expiration, default = 86400 (24 hours), max_uses: number +total number of uses allowed, default = 0 (unlimited), temporary: boolean whether +the invite grants temporary membership, default = false, unique: boolean whether +a unique code should be guaranteed, default = false +]=] +function GuildChannel:createInvite(payload) + local data, err = self.client._api:createChannelInvite(self._id, payload) + if data then + return Invite(data, self.client) + else + return nil, err + end +end + +--[=[ +@m getInvites +@t http +@r Cache +@d Returns a newly constructed cache of all invite objects for the channel. The +cache and its objects are not automatically updated via gateway events. You must +call this method again to get the updated objects. +]=] +function GuildChannel:getInvites() + local data, err = self.client._api:getChannelInvites(self._id) + if data then + return Cache(data, Invite, self.client) + else + return nil, err + end +end + +--[=[ +@m getPermissionOverwriteFor +@t mem +@p obj Role/Member +@r PermissionOverwrite +@d Returns a permission overwrite object corresponding to the provided member or +role object. If a cached overwrite is not found, an empty overwrite with +zero-permissions is returned instead. Therefore, this can be used to create a +new overwrite when one does not exist. Note that the member or role must exist +in the same guild as the channel does. +]=] +function GuildChannel:getPermissionOverwriteFor(obj) + local id, type + if isInstance(obj, classes.Role) and self._parent == obj._parent then + id, type = obj._id, 'role' + elseif isInstance(obj, classes.Member) and self._parent == obj._parent then + id, type = obj._user._id, 'member' + else + return nil, 'Invalid Role or Member: ' .. tostring(obj) + end + local overwrites = self._permission_overwrites + return overwrites:get(id) or overwrites:_insert(setmetatable({ + id = id, type = type, allow = 0, deny = 0 + }, {__jsontype = 'object'})) +end + +--[=[ +@m delete +@t http +@r boolean +@d Permanently deletes the channel. This cannot be undone! +]=] +function GuildChannel:delete() + return self:_delete() +end + +--[=[@p permissionOverwrites Cache An iterable cache of all overwrites that exist in this channel. To access an +overwrite that may exist, but is not cached, use `GuildChannel:getPermissionOverwriteFor`.]=] +function get.permissionOverwrites(self) + return self._permission_overwrites +end + +--[=[@p name string The name of the channel. This should be between 2 and 100 characters in length.]=] +function get.name(self) + return self._name +end + +--[=[@p position number The position of the channel, where 0 is the highest.]=] +function get.position(self) + return self._position +end + +--[=[@p guild Guild The guild in which this channel exists.]=] +function get.guild(self) + return self._parent +end + +--[=[@p category GuildCategoryChannel/nil The parent channel category that may contain this channel.]=] +function get.category(self) + return self._parent._categories:get(self._parent_id) +end + +--[=[@p private boolean Whether the "everyone" role has permission to view this +channel. In the Discord channel, private text channels are indicated with a lock +icon and private voice channels are not visible.]=] +function get.private(self) + local overwrite = self._permission_overwrites:get(self._parent._id) + return overwrite and overwrite:getDeniedPermissions():has('readMessages') +end + +return GuildChannel diff --git a/deps/discordia/libs/containers/abstract/Snowflake.lua b/deps/discordia/libs/containers/abstract/Snowflake.lua new file mode 100644 index 0000000..8b36de6 --- /dev/null +++ b/deps/discordia/libs/containers/abstract/Snowflake.lua @@ -0,0 +1,63 @@ +--[=[ +@c Snowflake x Container +@t abc +@d Defines the base methods and/or properties for all Discord objects that have +a Snowflake ID. +]=] + +local Date = require('utils/Date') +local Container = require('containers/abstract/Container') + +local Snowflake, get = require('class')('Snowflake', Container) + +function Snowflake:__init(data, parent) + Container.__init(self, data, parent) +end + +--[=[ +@m __hash +@r string +@d Returns `Snowflake.id` +]=] +function Snowflake:__hash() + return self._id +end + +--[=[ +@m getDate +@t mem +@r Date +@d Returns a unique Date object that represents when the object was created by Discord. + +Equivalent to `Date.fromSnowflake(Snowflake.id)` +]=] +function Snowflake:getDate() + return Date.fromSnowflake(self._id) +end + +--[=[@p id string The Snowflake ID that can be used to identify the object. This is guaranteed to +be unique except in cases where an object shares the ID of its parent.]=] +function get.id(self) + return self._id +end + +--[=[@p createdAt number The Unix time in seconds at which this object was created by Discord. Additional +decimal points may be present, though only the first 3 (milliseconds) should be +considered accurate. + +Equivalent to `Date.parseSnowflake(Snowflake.id)`. +]=] +function get.createdAt(self) + return Date.parseSnowflake(self._id) +end + +--[=[@p timestamp string The date and time at which this object was created by Discord, represented as +an ISO 8601 string plus microseconds when available. + +Equivalent to `Date.fromSnowflake(Snowflake.id):toISO()`. +]=] +function get.timestamp(self) + return Date.fromSnowflake(self._id):toISO() +end + +return Snowflake diff --git a/deps/discordia/libs/containers/abstract/TextChannel.lua b/deps/discordia/libs/containers/abstract/TextChannel.lua new file mode 100644 index 0000000..5cbd7ba --- /dev/null +++ b/deps/discordia/libs/containers/abstract/TextChannel.lua @@ -0,0 +1,326 @@ +--[=[ +@c TextChannel x Channel +@t abc +@d Defines the base methods and properties for all Discord text channels. +]=] + +local pathjoin = require('pathjoin') +local Channel = require('containers/abstract/Channel') +local Message = require('containers/Message') +local WeakCache = require('iterables/WeakCache') +local SecondaryCache = require('iterables/SecondaryCache') +local Resolver = require('client/Resolver') +local fs = require('fs') + +local splitPath = pathjoin.splitPath +local insert, remove, concat = table.insert, table.remove, table.concat +local format = string.format +local readFileSync = fs.readFileSync + +local TextChannel, get = require('class')('TextChannel', Channel) + +function TextChannel:__init(data, parent) + Channel.__init(self, data, parent) + self._messages = WeakCache({}, Message, self) +end + +--[=[ +@m getMessage +@t http +@p id Message-ID-Resolvable +@r Message +@d Gets a message object by ID. If the object is already cached, then the cached +object will be returned; otherwise, an HTTP request is made. +]=] +function TextChannel:getMessage(id) + id = Resolver.messageId(id) + local message = self._messages:get(id) + if message then + return message + else + local data, err = self.client._api:getChannelMessage(self._id, id) + if data then + return self._messages:_insert(data) + else + return nil, err + end + end +end + +--[=[ +@m getFirstMessage +@t http +@r Message +@d Returns the first message found in the channel, if any exist. This is not a +cache shortcut; an HTTP request is made each time this method is called. +]=] +function TextChannel:getFirstMessage() + local data, err = self.client._api:getChannelMessages(self._id, {after = self._id, limit = 1}) + if data then + if data[1] then + return self._messages:_insert(data[1]) + else + return nil, 'Channel has no messages' + end + else + return nil, err + end +end + +--[=[ +@m getLastMessage +@t http +@r Message +@d Returns the last message found in the channel, if any exist. This is not a +cache shortcut; an HTTP request is made each time this method is called. +]=] +function TextChannel:getLastMessage() + local data, err = self.client._api:getChannelMessages(self._id, {limit = 1}) + if data then + if data[1] then + return self._messages:_insert(data[1]) + else + return nil, 'Channel has no messages' + end + else + return nil, err + end +end + +local function getMessages(self, query) + local data, err = self.client._api:getChannelMessages(self._id, query) + if data then + return SecondaryCache(data, self._messages) + else + return nil, err + end +end + +--[=[ +@m getMessages +@t http +@op limit number +@r SecondaryCache +@d Returns a newly constructed cache of between 1 and 100 (default = 50) message +objects found in the channel. While the cache will never automatically gain or +lose objects, the objects that it contains may be updated by gateway events. +]=] +function TextChannel:getMessages(limit) + return getMessages(self, limit and {limit = limit}) +end + +--[=[ +@m getMessagesAfter +@t http +@p id Message-ID-Resolvable +@op limit number +@r SecondaryCache +@d Returns a newly constructed cache of between 1 and 100 (default = 50) message +objects found in the channel after a specific id. While the cache will never +automatically gain or lose objects, the objects that it contains may be updated +by gateway events. +]=] +function TextChannel:getMessagesAfter(id, limit) + id = Resolver.messageId(id) + return getMessages(self, {after = id, limit = limit}) +end + +--[=[ +@m getMessagesBefore +@t http +@p id Message-ID-Resolvable +@op limit number +@r SecondaryCache +@d Returns a newly constructed cache of between 1 and 100 (default = 50) message +objects found in the channel before a specific id. While the cache will never +automatically gain or lose objects, the objects that it contains may be updated +by gateway events. +]=] +function TextChannel:getMessagesBefore(id, limit) + id = Resolver.messageId(id) + return getMessages(self, {before = id, limit = limit}) +end + +--[=[ +@m getMessagesAround +@t http +@p id Message-ID-Resolvable +@op limit number +@r SecondaryCache +@d Returns a newly constructed cache of between 1 and 100 (default = 50) message +objects found in the channel around a specific point. While the cache will never +automatically gain or lose objects, the objects that it contains may be updated +by gateway events. +]=] +function TextChannel:getMessagesAround(id, limit) + id = Resolver.messageId(id) + return getMessages(self, {around = id, limit = limit}) +end + +--[=[ +@m getPinnedMessages +@t http +@r SecondaryCache +@d Returns a newly constructed cache of up to 50 messages that are pinned in the +channel. While the cache will never automatically gain or lose objects, the +objects that it contains may be updated by gateway events. +]=] +function TextChannel:getPinnedMessages() + local data, err = self.client._api:getPinnedMessages(self._id) + if data then + return SecondaryCache(data, self._messages) + else + return nil, err + end +end + +--[=[ +@m broadcastTyping +@t http +@r boolean +@d Indicates in the channel that the client's user "is typing". +]=] +function TextChannel:broadcastTyping() + local data, err = self.client._api:triggerTypingIndicator(self._id) + if data then + return true + else + return false, err + end +end + +local function parseFile(obj, files) + if type(obj) == 'string' then + local data, err = readFileSync(obj) + if not data then + return nil, err + end + files = files or {} + insert(files, {remove(splitPath(obj)), data}) + elseif type(obj) == 'table' and type(obj[1]) == 'string' and type(obj[2]) == 'string' then + files = files or {} + insert(files, obj) + else + return nil, 'Invalid file object: ' .. tostring(obj) + end + return files +end + +local function parseMention(obj, mentions) + if type(obj) == 'table' and obj.mentionString then + mentions = mentions or {} + insert(mentions, obj.mentionString) + else + return nil, 'Unmentionable object: ' .. tostring(obj) + end + return mentions +end + +--[=[ +@m send +@t http +@p content string/table +@r Message +@d Sends a message to the channel. If `content` is a string, then this is simply +sent as the message content. If it is a table, more advanced formatting is +allowed. See [[managing messages]] for more information. +]=] +function TextChannel:send(content) + + local data, err + + if type(content) == 'table' then + + local tbl = content + content = tbl.content + + if type(tbl.code) == 'string' then + content = format('```%s\n%s\n```', tbl.code, content) + elseif tbl.code == true then + content = format('```\n%s\n```', content) + end + + local mentions + if tbl.mention then + mentions, err = parseMention(tbl.mention) + if err then + return nil, err + end + end + if type(tbl.mentions) == 'table' then + for _, mention in ipairs(tbl.mentions) do + mentions, err = parseMention(mention, mentions) + if err then + return nil, err + end + end + end + + if mentions then + insert(mentions, content) + content = concat(mentions, ' ') + end + + local files + if tbl.file then + files, err = parseFile(tbl.file) + if err then + return nil, err + end + end + if type(tbl.files) == 'table' then + for _, file in ipairs(tbl.files) do + files, err = parseFile(file, files) + if err then + return nil, err + end + end + end + + data, err = self.client._api:createMessage(self._id, { + content = content, + tts = tbl.tts, + nonce = tbl.nonce, + embed = tbl.embed, + }, files) + + else + + data, err = self.client._api:createMessage(self._id, {content = content}) + + end + + if data then + return self._messages:_insert(data) + else + return nil, err + end + +end + +--[=[ +@m sendf +@t http +@p content string +@p ... * +@r Message +@d Sends a message to the channel with content formatted with `...` via `string.format` +]=] +function TextChannel:sendf(content, ...) + local data, err = self.client._api:createMessage(self._id, {content = format(content, ...)}) + if data then + return self._messages:_insert(data) + else + return nil, err + end +end + +--[=[@p messages WeakCache An iterable weak cache of all messages that are +visible to the client. Messages that are not referenced elsewhere are eventually +garbage collected. To access a message that may exist but is not cached, +use `TextChannel:getMessage`.]=] +function get.messages(self) + return self._messages +end + +return TextChannel diff --git a/deps/discordia/libs/containers/abstract/UserPresence.lua b/deps/discordia/libs/containers/abstract/UserPresence.lua new file mode 100644 index 0000000..022e615 --- /dev/null +++ b/deps/discordia/libs/containers/abstract/UserPresence.lua @@ -0,0 +1,127 @@ +--[=[ +@c UserPresence x Container +@t abc +@d Defines the base methods and/or properties for classes that represent a +user's current presence information. Note that any method or property that +exists for the User class is also available in the UserPresence class and its +subclasses. +]=] + +local null = require('json').null +local User = require('containers/User') +local Activity = require('containers/Activity') +local Container = require('containers/abstract/Container') + +local UserPresence, get = require('class')('UserPresence', Container) + +function UserPresence:__init(data, parent) + Container.__init(self, data, parent) + self._user = self.client._users:_insert(data.user) +end + +--[=[ +@m __hash +@r string +@d Returns `UserPresence.user.id` +]=] +function UserPresence:__hash() + return self._user._id +end + +local activities = setmetatable({}, {__mode = 'v'}) + +function UserPresence:_loadPresence(presence) + self._status = presence.status + local status = presence.client_status + if status then + self._web_status = status.web + self._mobile_status = status.mobile + self._desktop_status = status.desktop + end + local game = presence.game + if game == null then + self._activity = nil + elseif game then + local arr = presence.activities + if arr and arr[2] then + for i = 2, #arr do + for k, v in pairs(arr[i]) do + game[k] = v + end + end + end + if self._activity then + self._activity:_load(game) + else + local activity = activities[self:__hash()] + if activity then + activity:_load(game) + else + activity = Activity(game, self) + activities[self:__hash()] = activity + end + self._activity = activity + end + end +end + +function get.gameName(self) + self.client:_deprecated(self.__name, 'gameName', 'activity.name') + return self._activity and self._activity._name +end + +function get.gameType(self) + self.client:_deprecated(self.__name, 'gameType', 'activity.type') + return self._activity and self._activity._type +end + +function get.gameURL(self) + self.client:_deprecated(self.__name, 'gameURL', 'activity.url') + return self._activity and self._activity._url +end + +--[=[@p status string The user's overall status (online, dnd, idle, offline).]=] +function get.status(self) + return self._status or 'offline' +end + +--[=[@p webStatus string The user's web status (online, dnd, idle, offline).]=] +function get.webStatus(self) + return self._web_status or 'offline' +end + +--[=[@p mobileStatus string The user's mobile status (online, dnd, idle, offline).]=] +function get.mobileStatus(self) + return self._mobile_status or 'offline' +end + +--[=[@p desktopStatus string The user's desktop status (online, dnd, idle, offline).]=] +function get.desktopStatus(self) + return self._desktop_status or 'offline' +end + +--[=[@p user User The user that this presence represents.]=] +function get.user(self) + return self._user +end + +--[=[@p activity Activity/nil The Activity that this presence represents.]=] +function get.activity(self) + return self._activity +end + +-- user shortcuts + +for k, v in pairs(User) do + UserPresence[k] = UserPresence[k] or function(self, ...) + return v(self._user, ...) + end +end + +for k, v in pairs(User.__getters) do + get[k] = get[k] or function(self) + return v(self._user) + end +end + +return UserPresence diff --git a/deps/discordia/libs/endpoints.lua b/deps/discordia/libs/endpoints.lua new file mode 100644 index 0000000..e893980 --- /dev/null +++ b/deps/discordia/libs/endpoints.lua @@ -0,0 +1,54 @@ +return { + CHANNEL = "/channels/%s", + CHANNEL_INVITES = "/channels/%s/invites", + CHANNEL_MESSAGE = "/channels/%s/messages/%s", + CHANNEL_MESSAGES = "/channels/%s/messages", + CHANNEL_MESSAGES_BULK_DELETE = "/channels/%s/messages/bulk-delete", + CHANNEL_MESSAGE_REACTION = "/channels/%s/messages/%s/reactions/%s", + CHANNEL_MESSAGE_REACTIONS = "/channels/%s/messages/%s/reactions", + CHANNEL_MESSAGE_REACTION_ME = "/channels/%s/messages/%s/reactions/%s/@me", + CHANNEL_MESSAGE_REACTION_USER = "/channels/%s/messages/%s/reactions/%s/%s", + CHANNEL_PERMISSION = "/channels/%s/permissions/%s", + CHANNEL_PIN = "/channels/%s/pins/%s", + CHANNEL_PINS = "/channels/%s/pins", + CHANNEL_RECIPIENT = "/channels/%s/recipients/%s", + CHANNEL_TYPING = "/channels/%s/typing", + CHANNEL_WEBHOOKS = "/channels/%s/webhooks", + GATEWAY = "/gateway", + GATEWAY_BOT = "/gateway/bot", + GUILD = "/guilds/%s", + GUILDS = "/guilds", + GUILD_AUDIT_LOGS = "/guilds/%s/audit-logs", + GUILD_BAN = "/guilds/%s/bans/%s", + GUILD_BANS = "/guilds/%s/bans", + GUILD_CHANNELS = "/guilds/%s/channels", + GUILD_EMBED = "/guilds/%s/embed", + GUILD_EMOJI = "/guilds/%s/emojis/%s", + GUILD_EMOJIS = "/guilds/%s/emojis", + GUILD_INTEGRATION = "/guilds/%s/integrations/%s", + GUILD_INTEGRATIONS = "/guilds/%s/integrations", + GUILD_INTEGRATION_SYNC = "/guilds/%s/integrations/%s/sync", + GUILD_INVITES = "/guilds/%s/invites", + GUILD_MEMBER = "/guilds/%s/members/%s", + GUILD_MEMBERS = "/guilds/%s/members", + GUILD_MEMBER_ME_NICK = "/guilds/%s/members/@me/nick", + GUILD_MEMBER_ROLE = "/guilds/%s/members/%s/roles/%s", + GUILD_PRUNE = "/guilds/%s/prune", + GUILD_REGIONS = "/guilds/%s/regions", + GUILD_ROLE = "/guilds/%s/roles/%s", + GUILD_ROLES = "/guilds/%s/roles", + GUILD_WEBHOOKS = "/guilds/%s/webhooks", + INVITE = "/invites/%s", + OAUTH2_APPLICATION_ME = "/oauth2/applications/@me", + USER = "/users/%s", + USER_ME = "/users/@me", + USER_ME_CHANNELS = "/users/@me/channels", + USER_ME_CONNECTIONS = "/users/@me/connections", + USER_ME_GUILD = "/users/@me/guilds/%s", + USER_ME_GUILDS = "/users/@me/guilds", + VOICE_REGIONS = "/voice/regions", + WEBHOOK = "/webhooks/%s", + WEBHOOK_TOKEN = "/webhooks/%s/%s", + WEBHOOK_TOKEN_GITHUB = "/webhooks/%s/%s/github", + WEBHOOK_TOKEN_SLACK = "/webhooks/%s/%s/slack", +} diff --git a/deps/discordia/libs/enums.lua b/deps/discordia/libs/enums.lua new file mode 100644 index 0000000..f3b4327 --- /dev/null +++ b/deps/discordia/libs/enums.lua @@ -0,0 +1,214 @@ +local function enum(tbl) + local call = {} + for k, v in pairs(tbl) do + if call[v] then + return error(string.format('enum clash for %q and %q', k, call[v])) + end + call[v] = k + end + return setmetatable({}, { + __call = function(_, k) + if call[k] then + return call[k] + else + return error('invalid enumeration: ' .. tostring(k)) + end + end, + __index = function(_, k) + if tbl[k] then + return tbl[k] + else + return error('invalid enumeration: ' .. tostring(k)) + end + end, + __pairs = function() + return next, tbl + end, + __newindex = function() + return error('cannot overwrite enumeration') + end, + }) +end + +local enums = {enum = enum} + +enums.defaultAvatar = enum { + blurple = 0, + gray = 1, + green = 2, + orange = 3, + red = 4, +} + +enums.notificationSetting = enum { + allMessages = 0, + onlyMentions = 1, +} + +enums.channelType = enum { + text = 0, + private = 1, + voice = 2, + group = 3, + category = 4, + news = 5, +} + +enums.webhookType = enum { + incoming = 1, + channelFollower = 2, +} + +enums.messageType = enum { + default = 0, + recipientAdd = 1, + recipientRemove = 2, + call = 3, + channelNameChange = 4, + channelIconchange = 5, + pinnedMessage = 6, + memberJoin = 7, + premiumGuildSubscription = 8, + premiumGuildSubscriptionTier1 = 9, + premiumGuildSubscriptionTier2 = 10, + premiumGuildSubscriptionTier3 = 11, +} + +enums.relationshipType = enum { + none = 0, + friend = 1, + blocked = 2, + pendingIncoming = 3, + pendingOutgoing = 4, +} + +enums.activityType = enum { + default = 0, + streaming = 1, + listening = 2, + custom = 4, +} + +enums.status = enum { + online = 'online', + idle = 'idle', + doNotDisturb = 'dnd', + invisible = 'invisible', +} + +enums.gameType = enum { -- NOTE: deprecated; use activityType + default = 0, + streaming = 1, + listening = 2, + custom = 4, +} + +enums.verificationLevel = enum { + none = 0, + low = 1, + medium = 2, + high = 3, -- (╯°□°)╯︵ ┻━┻ + veryHigh = 4, -- ┻━┻ ミヽ(ಠ益ಠ)ノ彡┻━┻ +} + +enums.explicitContentLevel = enum { + none = 0, + medium = 1, + high = 2, +} + +enums.premiumTier = enum { + none = 0, + tier1 = 1, + tier2 = 2, + tier3 = 3, +} + +enums.permission = enum { + createInstantInvite = 0x00000001, + kickMembers = 0x00000002, + banMembers = 0x00000004, + administrator = 0x00000008, + manageChannels = 0x00000010, + manageGuild = 0x00000020, + addReactions = 0x00000040, + viewAuditLog = 0x00000080, + prioritySpeaker = 0x00000100, + stream = 0x00000200, + readMessages = 0x00000400, + sendMessages = 0x00000800, + sendTextToSpeech = 0x00001000, + manageMessages = 0x00002000, + embedLinks = 0x00004000, + attachFiles = 0x00008000, + readMessageHistory = 0x00010000, + mentionEveryone = 0x00020000, + useExternalEmojis = 0x00040000, + connect = 0x00100000, + speak = 0x00200000, + muteMembers = 0x00400000, + deafenMembers = 0x00800000, + moveMembers = 0x01000000, + useVoiceActivity = 0x02000000, + changeNickname = 0x04000000, + manageNicknames = 0x08000000, + manageRoles = 0x10000000, + manageWebhooks = 0x20000000, + manageEmojis = 0x40000000, +} + +enums.messageFlag = enum { + crossposted = 0x00000001, + isCrosspost = 0x00000002, + suppressEmbeds = 0x00000004, + sourceMessageDeleted = 0x00000008, + urgent = 0x00000010, +} + +enums.actionType = enum { + guildUpdate = 1, + channelCreate = 10, + channelUpdate = 11, + channelDelete = 12, + channelOverwriteCreate = 13, + channelOverwriteUpdate = 14, + channelOverwriteDelete = 15, + memberKick = 20, + memberPrune = 21, + memberBanAdd = 22, + memberBanRemove = 23, + memberUpdate = 24, + memberRoleUpdate = 25, + memberMove = 26, + memberDisconnect = 27, + botAdd = 28, + roleCreate = 30, + roleUpdate = 31, + roleDelete = 32, + inviteCreate = 40, + inviteUpdate = 41, + inviteDelete = 42, + webhookCreate = 50, + webhookUpdate = 51, + webhookDelete = 52, + emojiCreate = 60, + emojiUpdate = 61, + emojiDelete = 62, + messageDelete = 72, + messageBulkDelete = 73, + messagePin = 74, + messageUnpin = 75, + integrationCreate = 80, + integrationUpdate = 81, + integrationDelete = 82, +} + +enums.logLevel = enum { + none = 0, + error = 1, + warning = 2, + info = 3, + debug = 4, +} + +return enums diff --git a/deps/discordia/libs/extensions.lua b/deps/discordia/libs/extensions.lua new file mode 100644 index 0000000..5cb835f --- /dev/null +++ b/deps/discordia/libs/extensions.lua @@ -0,0 +1,253 @@ +--[[ NOTE: +These standard library extensions are NOT used in Discordia. They are here as a +convenience for those who wish to use them. + +There are multiple ways to implement some of these commonly used functions. +Please pay attention to the implementations used here and make sure that they +match your expectations. + +You may freely add to, remove, or edit any of the code here without any effect +on the rest of the library. If you do make changes, do be careful when sharing +your expectations with other users. + +You can inject these extensions into the standard Lua global tables by +calling either the main module (ex: discordia.extensions()) or each sub-module +(ex: discordia.extensions.string()) +]] + +local sort, concat = table.sort, table.concat +local insert, remove = table.insert, table.remove +local byte, char = string.byte, string.char +local gmatch, match = string.gmatch, string.match +local rep, find, sub = string.rep, string.find, string.sub +local min, max, random = math.min, math.max, math.random +local ceil, floor = math.ceil, math.floor + +local table = {} + +function table.count(tbl) + local n = 0 + for _ in pairs(tbl) do + n = n + 1 + end + return n +end + +function table.deepcount(tbl) + local n = 0 + for _, v in pairs(tbl) do + n = type(v) == 'table' and n + table.deepcount(v) or n + 1 + end + return n +end + +function table.copy(tbl) + local ret = {} + for k, v in pairs(tbl) do + ret[k] = v + end + return ret +end + +function table.deepcopy(tbl) + local ret = {} + for k, v in pairs(tbl) do + ret[k] = type(v) == 'table' and table.deepcopy(v) or v + end + return ret +end + +function table.reverse(tbl) + for i = 1, #tbl do + insert(tbl, i, remove(tbl)) + end +end + +function table.reversed(tbl) + local ret = {} + for i = #tbl, 1, -1 do + insert(ret, tbl[i]) + end + return ret +end + +function table.keys(tbl) + local ret = {} + for k in pairs(tbl) do + insert(ret, k) + end + return ret +end + +function table.values(tbl) + local ret = {} + for _, v in pairs(tbl) do + insert(ret, v) + end + return ret +end + +function table.randomipair(tbl) + local i = random(#tbl) + return i, tbl[i] +end + +function table.randompair(tbl) + local rand = random(table.count(tbl)) + local n = 0 + for k, v in pairs(tbl) do + n = n + 1 + if n == rand then + return k, v + end + end +end + +function table.sorted(tbl, fn) + local ret = {} + for i, v in ipairs(tbl) do + ret[i] = v + end + sort(ret, fn) + return ret +end + +function table.search(tbl, value) + for k, v in pairs(tbl) do + if v == value then + return k + end + end + return nil +end + +function table.slice(tbl, start, stop, step) + local ret = {} + for i = start or 1, stop or #tbl, step or 1 do + insert(ret, tbl[i]) + end + return ret +end + +local string = {} + +function string.split(str, delim) + local ret = {} + if not str then + return ret + end + if not delim or delim == '' then + for c in gmatch(str, '.') do + insert(ret, c) + end + return ret + end + local n = 1 + while true do + local i, j = find(str, delim, n) + if not i then break end + insert(ret, sub(str, n, i - 1)) + n = j + 1 + end + insert(ret, sub(str, n)) + return ret +end + +function string.trim(str) + return match(str, '^%s*(.-)%s*$') +end + +function string.pad(str, len, align, pattern) + pattern = pattern or ' ' + if align == 'right' then + return rep(pattern, (len - #str) / #pattern) .. str + elseif align == 'center' then + local pad = 0.5 * (len - #str) / #pattern + return rep(pattern, floor(pad)) .. str .. rep(pattern, ceil(pad)) + else -- left + return str .. rep(pattern, (len - #str) / #pattern) + end +end + +function string.startswith(str, pattern, plain) + local start = 1 + return find(str, pattern, start, plain) == start +end + +function string.endswith(str, pattern, plain) + local start = #str - #pattern + 1 + return find(str, pattern, start, plain) == start +end + +function string.levenshtein(str1, str2) + + if str1 == str2 then return 0 end + + local len1 = #str1 + local len2 = #str2 + + if len1 == 0 then + return len2 + elseif len2 == 0 then + return len1 + end + + local matrix = {} + for i = 0, len1 do + matrix[i] = {[0] = i} + end + for j = 0, len2 do + matrix[0][j] = j + end + + for i = 1, len1 do + for j = 1, len2 do + local cost = byte(str1, i) == byte(str2, j) and 0 or 1 + matrix[i][j] = min(matrix[i-1][j] + 1, matrix[i][j-1] + 1, matrix[i-1][j-1] + cost) + end + end + + return matrix[len1][len2] + +end + +function string.random(len, mn, mx) + local ret = {} + mn = mn or 0 + mx = mx or 255 + for _ = 1, len do + insert(ret, char(random(mn, mx))) + end + return concat(ret) +end + +local math = {} + +function math.clamp(n, minValue, maxValue) + return min(max(n, minValue), maxValue) +end + +function math.round(n, i) + local m = 10 ^ (i or 0) + return floor(n * m + 0.5) / m +end + +local ext = setmetatable({ + table = table, + string = string, + math = math, +}, {__call = function(self) + for _, v in pairs(self) do + v() + end +end}) + +for n, m in pairs(ext) do + setmetatable(m, {__call = function(self) + for k, v in pairs(self) do + _G[n][k] = v + end + end}) +end + +return ext diff --git a/deps/discordia/libs/iterables/ArrayIterable.lua b/deps/discordia/libs/iterables/ArrayIterable.lua new file mode 100644 index 0000000..1a4dede --- /dev/null +++ b/deps/discordia/libs/iterables/ArrayIterable.lua @@ -0,0 +1,108 @@ +--[=[ +@c ArrayIterable x Iterable +@mt mem +@d Iterable class that contains objects in a constant, ordered fashion, although +the order may change if the internal array is modified. Some versions may use a +map function to shape the objects before they are accessed. +]=] + +local Iterable = require('iterables/Iterable') + +local ArrayIterable, get = require('class')('ArrayIterable', Iterable) + +function ArrayIterable:__init(array, map) + self._array = array + self._map = map +end + +function ArrayIterable:__len() + local array = self._array + if not array or #array == 0 then + return 0 + end + local map = self._map + if map then -- map can return nil + return Iterable.__len(self) + else + return #array + end +end + +--[=[@p first * The first object in the array]=] +function get.first(self) + local array = self._array + if not array or #array == 0 then + return nil + end + local map = self._map + if map then + for i = 1, #array, 1 do + local v = array[i] + local obj = v and map(v) + if obj then + return obj + end + end + else + return array[1] + end +end + +--[=[@p last * The last object in the array]=] +function get.last(self) + local array = self._array + if not array or #array == 0 then + return nil + end + local map = self._map + if map then + for i = #array, 1, -1 do + local v = array[i] + local obj = v and map(v) + if obj then + return obj + end + end + else + return array[#array] + end +end + +--[=[ +@m iter +@r function +@d Returns an iterator for all contained objects in a consistent order. +]=] +function ArrayIterable:iter() + local array = self._array + if not array or #array == 0 then + return function() -- new closure for consistency + return nil + end + end + local map = self._map + if map then + local i = 0 + return function() + while true do + i = i + 1 + local v = array[i] + if not v then + return nil + end + v = map(v) + if v then + return v + end + end + end + else + local i = 0 + return function() + i = i + 1 + return array[i] + end + end +end + +return ArrayIterable diff --git a/deps/discordia/libs/iterables/Cache.lua b/deps/discordia/libs/iterables/Cache.lua new file mode 100644 index 0000000..a9c757b --- /dev/null +++ b/deps/discordia/libs/iterables/Cache.lua @@ -0,0 +1,150 @@ +--[=[ +@c Cache x Iterable +@mt mem +@d Iterable class that holds references to Discordia Class objects in no particular order. +]=] + +local json = require('json') +local Iterable = require('iterables/Iterable') + +local null = json.null + +local Cache = require('class')('Cache', Iterable) + +local meta = {__mode = 'v'} + +function Cache:__init(array, constructor, parent) + local objects = {} + for _, data in ipairs(array) do + local obj = constructor(data, parent) + objects[obj:__hash()] = obj + end + self._count = #array + self._objects = objects + self._constructor = constructor + self._parent = parent + self._deleted = setmetatable({}, meta) +end + +function Cache:__pairs() + return next, self._objects +end + +function Cache:__len() + return self._count +end + +local function insert(self, k, obj) + self._objects[k] = obj + self._count = self._count + 1 + return obj +end + +local function remove(self, k, obj) + self._objects[k] = nil + self._deleted[k] = obj + self._count = self._count - 1 + return obj +end + +local function hash(data) + -- local meta = getmetatable(data) -- debug + -- assert(meta and meta.__jsontype == 'object') -- debug + if data.id then -- snowflakes + return data.id + elseif data.user then -- members + return data.user.id + elseif data.emoji then -- reactions + return data.emoji.id ~= null and data.emoji.id or data.emoji.name + elseif data.code then -- invites + return data.code + else + return nil, 'json data could not be hashed' + end +end + +function Cache:_insert(data) + local k = assert(hash(data)) + local old = self._objects[k] + if old then + old:_load(data) + return old + elseif self._deleted[k] then + return insert(self, k, self._deleted[k]) + else + local obj = self._constructor(data, self._parent) + return insert(self, k, obj) + end +end + +function Cache:_remove(data) + local k = assert(hash(data)) + local old = self._objects[k] + if old then + old:_load(data) + return remove(self, k, old) + elseif self._deleted[k] then + return self._deleted[k] + else + return self._constructor(data, self._parent) + end +end + +function Cache:_delete(k) + local old = self._objects[k] + if old then + return remove(self, k, old) + elseif self._deleted[k] then + return self._deleted[k] + else + return nil + end +end + +function Cache:_load(array, update) + if update then + local updated = {} + for _, data in ipairs(array) do + local obj = self:_insert(data) + updated[obj:__hash()] = true + end + for obj in self:iter() do + local k = obj:__hash() + if not updated[k] then + self:_delete(k) + end + end + else + for _, data in ipairs(array) do + self:_insert(data) + end + end +end + +--[=[ +@m get +@p k * +@r * +@d Returns an individual object by key, where the key should match the result of +calling `__hash` on the contained objects. Unlike Iterable:get, this +method operates with O(1) complexity. +]=] +function Cache:get(k) + return self._objects[k] +end + +--[=[ +@m iter +@r function +@d Returns an iterator that returns all contained objects. The order of the objects +is not guaranteed. +]=] +function Cache:iter() + local objects, k, obj = self._objects + return function() + k, obj = next(objects, k) + return obj + end +end + +return Cache diff --git a/deps/discordia/libs/iterables/FilteredIterable.lua b/deps/discordia/libs/iterables/FilteredIterable.lua new file mode 100644 index 0000000..5fdfe1e --- /dev/null +++ b/deps/discordia/libs/iterables/FilteredIterable.lua @@ -0,0 +1,27 @@ +--[=[ +@c FilteredIterable x Iterable +@mt mem +@d Iterable class that wraps another iterable and serves a subset of the objects +that the original iterable contains. +]=] + +local Iterable = require('iterables/Iterable') + +local FilteredIterable = require('class')('FilteredIterable', Iterable) + +function FilteredIterable:__init(base, predicate) + self._base = base + self._predicate = predicate +end + +--[=[ +@m iter +@r function +@d Returns an iterator that returns all contained objects. The order of the objects +is not guaranteed. +]=] +function FilteredIterable:iter() + return self._base:findAll(self._predicate) +end + +return FilteredIterable diff --git a/deps/discordia/libs/iterables/Iterable.lua b/deps/discordia/libs/iterables/Iterable.lua new file mode 100644 index 0000000..3916372 --- /dev/null +++ b/deps/discordia/libs/iterables/Iterable.lua @@ -0,0 +1,278 @@ +--[=[ +@c Iterable +@mt mem +@d Abstract base class that defines the base methods and properties for a +general purpose data structure with features that are better suited for an +object-oriented environment. + +Note: All sub-classes should implement their own `__init` and `iter` methods and +all stored objects should have a `__hash` method. +]=] + +local random = math.random +local insert, sort, pack = table.insert, table.sort, table.pack + +local Iterable = require('class')('Iterable') + +--[=[ +@m __pairs +@r function +@d Defines the behavior of the `pairs` function. Returns an iterator that returns +a `key, value` pair, where `key` is the result of calling `__hash` on the `value`. +]=] +function Iterable:__pairs() + local gen = self:iter() + return function() + local obj = gen() + if not obj then + return nil + end + return obj:__hash(), obj + end +end + +--[=[ +@m __len +@r function +@d Defines the behavior of the `#` operator. Returns the total number of objects +stored in the iterable. +]=] +function Iterable:__len() + local n = 0 + for _ in self:iter() do + n = n + 1 + end + return n +end + +--[=[ +@m get +@p k * +@r * +@d Returns an individual object by key, where the key should match the result of +calling `__hash` on the contained objects. Operates with up to O(n) complexity. +]=] +function Iterable:get(k) -- objects must be hashable + for obj in self:iter() do + if obj:__hash() == k then + return obj + end + end + return nil +end + +--[=[ +@m find +@p fn function +@r * +@d Returns the first object that satisfies a predicate. +]=] +function Iterable:find(fn) + for obj in self:iter() do + if fn(obj) then + return obj + end + end + return nil +end + +--[=[ +@m findAll +@p fn function +@r function +@d Returns an iterator that returns all objects that satisfy a predicate. +]=] +function Iterable:findAll(fn) + local gen = self:iter() + return function() + while true do + local obj = gen() + if not obj then + return nil + end + if fn(obj) then + return obj + end + end + end +end + +--[=[ +@m forEach +@p fn function +@r nil +@d Iterates through all objects and calls a function `fn` that takes the +objects as an argument. +]=] +function Iterable:forEach(fn) + for obj in self:iter() do + fn(obj) + end +end + +--[=[ +@m random +@r * +@d Returns a random object that is contained in the iterable. +]=] +function Iterable:random() + local n = 1 + local rand = random(#self) + for obj in self:iter() do + if n == rand then + return obj + end + n = n + 1 + end +end + +--[=[ +@m count +@op fn function +@r number +@d If a predicate is provided, this returns the number of objects in the iterable +that satistfy the predicate; otherwise, the total number of objects. +]=] +function Iterable:count(fn) + if not fn then + return self:__len() + end + local n = 0 + for _ in self:findAll(fn) do + n = n + 1 + end + return n +end + +local function sorter(a, b) + local t1, t2 = type(a), type(b) + if t1 == 'string' then + if t2 == 'string' then + local n1 = tonumber(a) + if n1 then + local n2 = tonumber(b) + if n2 then + return n1 < n2 + end + end + return a:lower() < b:lower() + elseif t2 == 'number' then + local n1 = tonumber(a) + if n1 then + return n1 < b + end + return a:lower() < tostring(b) + end + elseif t1 == 'number' then + if t2 == 'number' then + return a < b + elseif t2 == 'string' then + local n2 = tonumber(b) + if n2 then + return a < n2 + end + return tostring(a) < b:lower() + end + end + local m1 = getmetatable(a) + if m1 and m1.__lt then + local m2 = getmetatable(b) + if m2 and m2.__lt then + return a < b + end + end + return tostring(a) < tostring(b) +end + +--[=[ +@m toArray +@op sortBy string +@op fn function +@r table +@d Returns a sequentially-indexed table that contains references to all objects. +If a `sortBy` string is provided, then the table is sorted by that particular +property. If a predicate is provided, then only objects that satisfy it will +be included. +]=] +function Iterable:toArray(sortBy, fn) + local t1 = type(sortBy) + if t1 == 'string' then + fn = type(fn) == 'function' and fn + elseif t1 == 'function' then + fn = sortBy + sortBy = nil + end + local ret = {} + for obj in self:iter() do + if not fn or fn(obj) then + insert(ret, obj) + end + end + if sortBy then + sort(ret, function(a, b) + return sorter(a[sortBy], b[sortBy]) + end) + end + return ret +end + +--[=[ +@m select +@p ... string +@r table +@d Similarly to an SQL query, this returns a sorted Lua table of rows where each +row corresponds to each object in the iterable, and each value in the row is +selected from the objects according to the keys provided. +]=] +function Iterable:select(...) + local rows = {} + local keys = pack(...) + for obj in self:iter() do + local row = {} + for i = 1, keys.n do + row[i] = obj[keys[i]] + end + insert(rows, row) + end + sort(rows, function(a, b) + for i = 1, keys.n do + if a[i] ~= b[i] then + return sorter(a[i], b[i]) + end + end + end) + return rows +end + +--[=[ +@m pick +@p ... string/function +@r function +@d This returns an iterator that, when called, returns the values from each +encountered object, picked by the provided keys. If a key is a string, the objects +are indexed with the string. If a key is a function, the function is called with +the object passed as its first argument. +]=] +function Iterable:pick(...) + local keys = pack(...) + local values = {} + local n = keys.n + local gen = self:iter() + return function() + local obj = gen() + if not obj then + return nil + end + for i = 1, n do + local k = keys[i] + if type(k) == 'function' then + values[i] = k(obj) + else + values[i] = obj[k] + end + end + return unpack(values, 1, n) + end +end + +return Iterable diff --git a/deps/discordia/libs/iterables/SecondaryCache.lua b/deps/discordia/libs/iterables/SecondaryCache.lua new file mode 100644 index 0000000..660fd2c --- /dev/null +++ b/deps/discordia/libs/iterables/SecondaryCache.lua @@ -0,0 +1,78 @@ +--[=[ +@c SecondaryCache x Iterable +@mt mem +@d Iterable class that wraps another cache. Objects added to or removed from a +secondary cache are also automatically added to or removed from the primary +cache that it wraps. +]=] + +local Iterable = require('iterables/Iterable') + +local SecondaryCache = require('class')('SecondaryCache', Iterable) + +function SecondaryCache:__init(array, primary) + local objects = {} + for _, data in ipairs(array) do + local obj = primary:_insert(data) + objects[obj:__hash()] = obj + end + self._count = #array + self._objects = objects + self._primary = primary +end + +function SecondaryCache:__pairs() + return next, self._objects +end + +function SecondaryCache:__len() + return self._count +end + +function SecondaryCache:_insert(data) + local obj = self._primary:_insert(data) + local k = obj:__hash() + if not self._objects[k] then + self._objects[k] = obj + self._count = self._count + 1 + end + return obj +end + +function SecondaryCache:_remove(data) + local obj = self._primary:_insert(data) -- yes, this is correct + local k = obj:__hash() + if self._objects[k] then + self._objects[k] = nil + self._count = self._count - 1 + end + return obj +end + +--[=[ +@m get +@p k * +@r * +@d Returns an individual object by key, where the key should match the result of +calling `__hash` on the contained objects. Unlike the default version, this +method operates with O(1) complexity. +]=] +function SecondaryCache:get(k) + return self._objects[k] +end + +--[=[ +@m iter +@r function +@d Returns an iterator that returns all contained objects. The order of the objects +is not guaranteed. +]=] +function SecondaryCache:iter() + local objects, k, obj = self._objects + return function() + k, obj = next(objects, k) + return obj + end +end + +return SecondaryCache diff --git a/deps/discordia/libs/iterables/TableIterable.lua b/deps/discordia/libs/iterables/TableIterable.lua new file mode 100644 index 0000000..699389d --- /dev/null +++ b/deps/discordia/libs/iterables/TableIterable.lua @@ -0,0 +1,53 @@ +--[=[ +@c TableIterable x Iterable +@mt mem +@d Iterable class that wraps a basic Lua table, where order is not guaranteed. +Some versions may use a map function to shape the objects before they are accessed. +]=] + +local Iterable = require('iterables/Iterable') + +local TableIterable = require('class')('TableIterable', Iterable) + +function TableIterable:__init(tbl, map) + self._tbl = tbl + self._map = map +end + +--[=[ +@m iter +@r function +@d Returns an iterator that returns all contained objects. The order of the objects is not guaranteed. +]=] +function TableIterable:iter() + local tbl = self._tbl + if not tbl then + return function() + return nil + end + end + local map = self._map + if map then + local k, v + return function() + while true do + k, v = next(tbl, k) + if not v then + return nil + end + v = map(v) + if v then + return v + end + end + end + else + local k, v + return function() + k, v = next(tbl, k) + return v + end + end +end + +return TableIterable diff --git a/deps/discordia/libs/iterables/WeakCache.lua b/deps/discordia/libs/iterables/WeakCache.lua new file mode 100644 index 0000000..8d89ba8 --- /dev/null +++ b/deps/discordia/libs/iterables/WeakCache.lua @@ -0,0 +1,25 @@ +--[=[ +@c WeakCache x Cache +@mt mem +@d Extends the functionality of a regular cache by making use of weak references +to the objects that are cached. If all references to an object are weak, as they +are here, then the object will be deleted on the next garbage collection cycle. +]=] + +local Cache = require('iterables/Cache') +local Iterable = require('iterables/Iterable') + +local WeakCache = require('class')('WeakCache', Cache) + +local meta = {__mode = 'v'} + +function WeakCache:__init(array, constructor, parent) + Cache.__init(self, array, constructor, parent) + setmetatable(self._objects, meta) +end + +function WeakCache:__len() -- NOTE: _count is not accurate for weak caches + return Iterable.__len(self) +end + +return WeakCache diff --git a/deps/discordia/libs/utils/Clock.lua b/deps/discordia/libs/utils/Clock.lua new file mode 100644 index 0000000..4912afa --- /dev/null +++ b/deps/discordia/libs/utils/Clock.lua @@ -0,0 +1,56 @@ +--[=[ +@c Clock x Emitter +@t ui +@mt mem +@d Used to periodically execute code according to the ticking of the system clock instead of arbitrary interval. +]=] + +local timer = require('timer') +local Emitter = require('utils/Emitter') + +local date = os.date +local setInterval, clearInterval = timer.setInterval, timer.clearInterval + +local Clock = require('class')('Clock', Emitter) + +function Clock:__init() + Emitter.__init(self) +end + +--[=[ +@m start +@op utc boolean +@r nil +@d Starts the main loop for the clock. If a truthy argument is passed, then UTC +time is used; otherwise, local time is used. As the clock ticks, an event is +emitted for every `os.date` value change. The event name is the key of the value +that changed and the event argument is the corresponding date table. +]=] +function Clock:start(utc) + if self._interval then return end + local fmt = utc and '!*t' or '*t' + local prev = date(fmt) + self._interval = setInterval(1000, function() + local now = date(fmt) + for k, v in pairs(now) do + if v ~= prev[k] then + self:emit(k, now) + end + end + prev = now + end) +end + +--[=[ +@m stop +@r nil +@d Stops the main loop for the clock. +]=] +function Clock:stop() + if self._interval then + clearInterval(self._interval) + self._interval = nil + end +end + +return Clock diff --git a/deps/discordia/libs/utils/Color.lua b/deps/discordia/libs/utils/Color.lua new file mode 100644 index 0000000..3d01bd9 --- /dev/null +++ b/deps/discordia/libs/utils/Color.lua @@ -0,0 +1,313 @@ +--[=[ +@c Color +@t ui +@mt mem +@p value number +@d Wrapper for 24-bit colors packed as a decimal value. See the static constructors for more information. +]=] + +local class = require('class') + +local format = string.format +local min, max, abs, floor = math.min, math.max, math.abs, math.floor +local lshift, rshift = bit.lshift, bit.rshift +local band, bor = bit.band, bit.bor +local bnot = bit.bnot +local isInstance = class.isInstance + +local Color, get = class('Color') + +local function check(self, other) + if not isInstance(self, Color) or not isInstance(other, Color) then + return error('Cannot perform operation with non-Color object', 2) + end +end + +local function clamp(n, mn, mx) + return min(max(n, mn), mx) +end + +function Color:__init(value) + value = tonumber(value) + self._value = value and band(value, 0xFFFFFF) or 0 +end + +function Color:__tostring() + return format('Color: %s (%i, %i, %i)', self:toHex(), self:toRGB()) +end + +function Color:__eq(other) check(self, other) + return self._value == other._value +end + +function Color:__add(other) check(self, other) + local r = clamp(self.r + other.r, 0, 0xFF) + local g = clamp(self.g + other.g, 0, 0xFF) + local b = clamp(self.b + other.b, 0, 0xFF) + return Color.fromRGB(r, g, b) +end + +function Color:__sub(other) check(self, other) + local r = clamp(self.r - other.r, 0, 0xFF) + local g = clamp(self.g - other.g, 0, 0xFF) + local b = clamp(self.b - other.b, 0, 0xFF) + return Color.fromRGB(r, g, b) +end + +function Color:__mul(other) + if not isInstance(self, Color) then + self, other = other, self + end + other = tonumber(other) + if other then + local r = clamp(self.r * other, 0, 0xFF) + local g = clamp(self.g * other, 0, 0xFF) + local b = clamp(self.b * other, 0, 0xFF) + return Color.fromRGB(r, g, b) + else + return error('Cannot perform operation with non-numeric object') + end +end + +function Color:__div(other) + if not isInstance(self, Color) then + return error('Division with Color is not commutative') + end + other = tonumber(other) + if other then + local r = clamp(self.r / other, 0, 0xFF) + local g = clamp(self.g / other, 0, 0xFF) + local b = clamp(self.b / other, 0, 0xFF) + return Color.fromRGB(r, g, b) + else + return error('Cannot perform operation with non-numeric object') + end +end + +--[=[ +@m fromHex +@t static +@p hex string +@r Color +@d Constructs a new Color object from a hexadecimal string. The string may or may +not be prefixed by `#`; all other characters are interpreted as a hex string. +]=] +function Color.fromHex(hex) + return Color(tonumber(hex:match('#?(.*)'), 16)) +end + +--[=[ +@m fromRGB +@t static +@p r number +@p g number +@p b number +@r Color +@d Constructs a new Color object from RGB values. Values are allowed to overflow +though one component will not overflow to the next component. +]=] +function Color.fromRGB(r, g, b) + r = band(lshift(r, 16), 0xFF0000) + g = band(lshift(g, 8), 0x00FF00) + b = band(b, 0x0000FF) + return Color(bor(bor(r, g), b)) +end + +local function fromHue(h, c, m) + local x = c * (1 - abs(h / 60 % 2 - 1)) + local r, g, b + if 0 <= h and h < 60 then + r, g, b = c, x, 0 + elseif 60 <= h and h < 120 then + r, g, b = x, c, 0 + elseif 120 <= h and h < 180 then + r, g, b = 0, c, x + elseif 180 <= h and h < 240 then + r, g, b = 0, x, c + elseif 240 <= h and h < 300 then + r, g, b = x, 0, c + elseif 300 <= h and h < 360 then + r, g, b = c, 0, x + end + r = (r + m) * 0xFF + g = (g + m) * 0xFF + b = (b + m) * 0xFF + return r, g, b +end + +local function toHue(r, g, b) + r = r / 0xFF + g = g / 0xFF + b = b / 0xFF + local mn = min(r, g, b) + local mx = max(r, g, b) + local d = mx - mn + local h + if d == 0 then + h = 0 + elseif mx == r then + h = (g - b) / d % 6 + elseif mx == g then + h = (b - r) / d + 2 + elseif mx == b then + h = (r - g) / d + 4 + end + h = floor(h * 60 + 0.5) + return h, d, mx, mn +end + +--[=[ +@m fromHSV +@t static +@p h number +@p s number +@p v number +@r Color +@d Constructs a new Color object from HSV values. Hue is allowed to overflow +while saturation and value are clamped to [0, 1]. +]=] +function Color.fromHSV(h, s, v) + h = h % 360 + s = clamp(s, 0, 1) + v = clamp(v, 0, 1) + local c = v * s + local m = v - c + local r, g, b = fromHue(h, c, m) + return Color.fromRGB(r, g, b) +end + +--[=[ +@m fromHSL +@t static +@p h number +@p s number +@p l number +@r Color +@d Constructs a new Color object from HSL values. Hue is allowed to overflow +while saturation and lightness are clamped to [0, 1]. +]=] +function Color.fromHSL(h, s, l) + h = h % 360 + s = clamp(s, 0, 1) + l = clamp(l, 0, 1) + local c = (1 - abs(2 * l - 1)) * s + local m = l - c * 0.5 + local r, g, b = fromHue(h, c, m) + return Color.fromRGB(r, g, b) +end + +--[=[ +@m toHex +@r string +@d Returns a 6-digit hexadecimal string that represents the color value. +]=] +function Color:toHex() + return format('#%06X', self._value) +end + +--[=[ +@m toRGB +@r number +@r number +@r number +@d Returns the red, green, and blue values that are packed into the color value. +]=] +function Color:toRGB() + return self.r, self.g, self.b +end + +--[=[ +@m toHSV +@r number +@r number +@r number +@d Returns the hue, saturation, and value that represents the color value. +]=] +function Color:toHSV() + local h, d, mx = toHue(self.r, self.g, self.b) + local v = mx + local s = mx == 0 and 0 or d / mx + return h, s, v +end + +--[=[ +@m toHSL +@r number +@r number +@r number +@d Returns the hue, saturation, and lightness that represents the color value. +]=] +function Color:toHSL() + local h, d, mx, mn = toHue(self.r, self.g, self.b) + local l = (mx + mn) * 0.5 + local s = d == 0 and 0 or d / (1 - abs(2 * l - 1)) + return h, s, l +end + +--[=[@p value number The raw decimal value that represents the color value.]=] +function get.value(self) + return self._value +end + +local function getByte(value, offset) + return band(rshift(value, offset), 0xFF) +end + +--[=[@p r number The value that represents the color's red-level.]=] +function get.r(self) + return getByte(self._value, 16) +end + +--[=[@p g number The value that represents the color's green-level.]=] +function get.g(self) + return getByte(self._value, 8) +end + +--[=[@p b number The value that represents the color's blue-level.]=] +function get.b(self) + return getByte(self._value, 0) +end + +local function setByte(value, offset, new) + local byte = lshift(0xFF, offset) + value = band(value, bnot(byte)) + return bor(value, band(lshift(new, offset), byte)) +end + +--[=[ +@m setRed +@r nil +@d Sets the color's red-level. +]=] +function Color:setRed(r) + self._value = setByte(self._value, 16, r) +end + +--[=[ +@m setGreen +@r nil +@d Sets the color's green-level. +]=] +function Color:setGreen(g) + self._value = setByte(self._value, 8, g) +end + +--[=[ +@m setBlue +@r nil +@d Sets the color's blue-level. +]=] +function Color:setBlue(b) + self._value = setByte(self._value, 0, b) +end + +--[=[ +@m copy +@r Color +@d Returns a new copy of the original color object. +]=] +function Color:copy() + return Color(self._value) +end + +return Color diff --git a/deps/discordia/libs/utils/Date.lua b/deps/discordia/libs/utils/Date.lua new file mode 100644 index 0000000..ec2ddfe --- /dev/null +++ b/deps/discordia/libs/utils/Date.lua @@ -0,0 +1,394 @@ +--[=[ +@c Date +@t ui +@mt mem +@op seconds number +@op microseconds number +@d Represents a single moment in time and provides utilities for converting to +and from different date and time formats. Although microsecond precision is available, +most formats are implemented with only second precision. +]=] + +local class = require('class') +local constants = require('constants') +local Time = require('utils/Time') + +local abs, modf, fmod, floor = math.abs, math.modf, math.fmod, math.floor +local format = string.format +local date, time, difftime = os.date, os.time, os.difftime +local isInstance = class.isInstance + +local MS_PER_S = constants.MS_PER_S +local US_PER_MS = constants.US_PER_MS +local US_PER_S = US_PER_MS * MS_PER_S + +local DISCORD_EPOCH = constants.DISCORD_EPOCH + +local months = { + Jan = 1, Feb = 2, Mar = 3, Apr = 4, May = 5, Jun = 6, + Jul = 7, Aug = 8, Sep = 9, Oct = 10, Nov = 11, Dec = 12 +} + +local function offset() -- difference between *t and !*t + return difftime(time(), time(date('!*t'))) +end + +local Date = class('Date') + +local function check(self, other) + if not isInstance(self, Date) or not isInstance(other, Date) then + return error('Cannot perform operation with non-Date object', 2) + end +end + +function Date:__init(seconds, micro) + + local f + seconds = tonumber(seconds) + if seconds then + seconds, f = modf(seconds) + else + seconds = time() + end + + micro = tonumber(micro) + if micro then + seconds = seconds + modf(micro / US_PER_S) + micro = fmod(micro, US_PER_S) + else + micro = 0 + end + + if f and f > 0 then + micro = micro + US_PER_S * f + end + + self._s = seconds + self._us = floor(micro + 0.5) + +end + +function Date:__tostring() + return 'Date: ' .. self:toString() +end + +--[=[ +@m toString +@op fmt string +@r string +@d Returns a string from this Date object via Lua's `os.date`. +If no format string is provided, the default is '%a %b %d %Y %T GMT%z (%Z)'. +]=] +function Date:toString(fmt) + if not fmt or fmt == '*t' or fmt == '!*t' then + fmt = '%a %b %d %Y %T GMT%z (%Z)' + end + return date(fmt, self._s) +end + +function Date:__eq(other) check(self, other) + return self._s == other._s and self._us == other._us +end + +function Date:__lt(other) check(self, other) + return self:toMicroseconds() < other:toMicroseconds() +end + +function Date:__le(other) check(self, other) + return self:toMicroseconds() <= other:toMicroseconds() +end + +function Date:__add(other) + if not isInstance(self, Date) then + self, other = other, self + end + if not isInstance(other, Time) then + return error('Cannot perform operation with non-Time object') + end + return Date(self:toSeconds() + other:toSeconds()) +end + +function Date:__sub(other) + if isInstance(self, Date) then + if isInstance(other, Date) then + return Time(abs(self:toMilliseconds() - other:toMilliseconds())) + elseif isInstance(other, Time) then + return Date(self:toSeconds() - other:toSeconds()) + else + return error('Cannot perform operation with non-Date/Time object') + end + else + return error('Cannot perform operation with non-Date object') + end +end + +--[=[ +@m parseISO +@t static +@p str string +@r number +@r number +@d Converts an ISO 8601 string into a Unix time in seconds. For compatibility +with Discord's timestamp format, microseconds are also provided as a second +return value. +]=] +function Date.parseISO(str) + local year, month, day, hour, min, sec, other = str:match( + '(%d+)-(%d+)-(%d+).(%d+):(%d+):(%d+)(.*)' + ) + other = other:match('%.%d+') + return Date.parseTableUTC { + day = day, month = month, year = year, + hour = hour, min = min, sec = sec, isdst = false, + }, other and other * US_PER_S or 0 +end + +--[=[ +@m parseHeader +@t static +@p str string +@r number +@d Converts an RFC 2822 string (an HTTP Date header) into a Unix time in seconds. +]=] +function Date.parseHeader(str) + local day, month, year, hour, min, sec = str:match( + '%a+, (%d+) (%a+) (%d+) (%d+):(%d+):(%d+) GMT' + ) + return Date.parseTableUTC { + day = day, month = months[month], year = year, + hour = hour, min = min, sec = sec, isdst = false, + } +end + +--[=[ +@m parseSnowflake +@t static +@p id string +@r number +@d Converts a Discord Snowflake ID into a Unix time in seconds. Additional +decimal points may be present, though only the first 3 (milliseconds) should be +considered accurate. +]=] +function Date.parseSnowflake(id) + return (id / 2^22 + DISCORD_EPOCH) / MS_PER_S +end + +--[=[ +@m parseTable +@t static +@p tbl table +@r number +@d Interprets a Lua date table as a local time and converts it to a Unix time in +seconds. Equivalent to `os.time(tbl)`. +]=] +function Date.parseTable(tbl) + return time(tbl) +end + +--[=[ +@m parseTableUTC +@t static +@p tbl table +@r number +@d Interprets a Lua date table as a UTC time and converts it to a Unix time in +seconds. Equivalent to `os.time(tbl)` with a correction for UTC. +]=] +function Date.parseTableUTC(tbl) + return time(tbl) + offset() +end + +--[=[ +@m fromISO +@t static +@p str string +@r Date +@d Constructs a new Date object from an ISO 8601 string. Equivalent to +`Date(Date.parseISO(str))`. +]=] +function Date.fromISO(str) + return Date(Date.parseISO(str)) +end + +--[=[ +@m fromHeader +@t static +@p str string +@r Date +@d Constructs a new Date object from an RFC 2822 string. Equivalent to +`Date(Date.parseHeader(str))`. +]=] +function Date.fromHeader(str) + return Date(Date.parseHeader(str)) +end + +--[=[ +@m fromSnowflake +@t static +@p id string +@r Date +@d Constructs a new Date object from a Discord/Twitter Snowflake ID. Equivalent to +`Date(Date.parseSnowflake(id))`. +]=] +function Date.fromSnowflake(id) + return Date(Date.parseSnowflake(id)) +end + +--[=[ +@m fromTable +@t static +@p tbl table +@r Date +@d Constructs a new Date object from a Lua date table interpreted as a local time. +Equivalent to `Date(Date.parseTable(tbl))`. +]=] +function Date.fromTable(tbl) + return Date(Date.parseTable(tbl)) +end + +--[=[ +@m fromTableUTC +@t static +@p tbl table +@r Date +@d Constructs a new Date object from a Lua date table interpreted as a UTC time. +Equivalent to `Date(Date.parseTableUTC(tbl))`. +]=] +function Date.fromTableUTC(tbl) + return Date(Date.parseTableUTC(tbl)) +end + +--[=[ +@m fromSeconds +@t static +@p s number +@r Date +@d Constructs a new Date object from a Unix time in seconds. +]=] +function Date.fromSeconds(s) + return Date(s) +end + +--[=[ +@m fromMilliseconds +@t static +@p ms number +@r Date +@d Constructs a new Date object from a Unix time in milliseconds. +]=] +function Date.fromMilliseconds(ms) + return Date(ms / MS_PER_S) +end + +--[=[ +@m fromMicroseconds +@t static +@p us number +@r Date +@d Constructs a new Date object from a Unix time in microseconds. +]=] +function Date.fromMicroseconds(us) + return Date(0, us) +end + +--[=[ +@m toISO +@op sep string +@op tz string +@r string +@d Returns an ISO 8601 string that represents the stored date and time. +If `sep` and `tz` are both provided, then they are used as a custom separator +and timezone; otherwise, `T` is used for the separator and `+00:00` is used for +the timezone, plus microseconds if available. +]=] +function Date:toISO(sep, tz) + if sep and tz then + local ret = date('!%F%%s%T%%s', self._s) + return format(ret, sep, tz) + else + if self._us == 0 then + return date('!%FT%T', self._s) .. '+00:00' + else + return date('!%FT%T', self._s) .. format('.%06i+00:00', self._us) + end + end +end + +--[=[ +@m toHeader +@r string +@d Returns an RFC 2822 string that represents the stored date and time. +]=] +function Date:toHeader() + return date('!%a, %d %b %Y %T GMT', self._s) +end + +--[=[ +@m toSnowflake +@r string +@d Returns a synthetic Discord Snowflake ID based on the stored date and time. +Due to the lack of native 64-bit support, the result may lack precision. +In other words, `Date.fromSnowflake(id):toSnowflake() == id` may be `false`. +]=] +function Date:toSnowflake() + local n = (self:toMilliseconds() - DISCORD_EPOCH) * 2^22 + return format('%f', n):match('%d*') +end + +--[=[ +@m toTable +@r table +@d Returns a Lua date table that represents the stored date and time as a local +time. Equivalent to `os.date('*t', s)` where `s` is the Unix time in seconds. +]=] +function Date:toTable() + return date('*t', self._s) +end + +--[=[ +@m toTableUTC +@r table +@d Returns a Lua date table that represents the stored date and time as a UTC +time. Equivalent to `os.date('!*t', s)` where `s` is the Unix time in seconds. +]=] +function Date:toTableUTC() + return date('!*t', self._s) +end + +--[=[ +@m toSeconds +@r number +@d Returns a Unix time in seconds that represents the stored date and time. +]=] +function Date:toSeconds() + return self._s + self._us / US_PER_S +end + +--[=[ +@m toMilliseconds +@r number +@d Returns a Unix time in milliseconds that represents the stored date and time. +]=] +function Date:toMilliseconds() + return self._s * MS_PER_S + self._us / US_PER_MS +end + +--[=[ +@m toMicroseconds +@r number +@d Returns a Unix time in microseconds that represents the stored date and time. +]=] +function Date:toMicroseconds() + return self._s * US_PER_S + self._us +end + +--[=[ +@m toParts +@r number +@r number +@d Returns the seconds and microseconds that are stored in the date object. +]=] +function Date:toParts() + return self._s, self._us +end + +return Date diff --git a/deps/discordia/libs/utils/Deque.lua b/deps/discordia/libs/utils/Deque.lua new file mode 100644 index 0000000..bfdabc0 --- /dev/null +++ b/deps/discordia/libs/utils/Deque.lua @@ -0,0 +1,105 @@ +--[=[ +@c Deque +@t ui +@mt mem +@d An implementation of a double-ended queue. +]=] + +local Deque = require('class')('Deque') + +function Deque:__init() + self._objects = {} + self._first = 0 + self._last = -1 +end + +--[=[ +@m getCount +@r number +@d Returns the total number of values stored. +]=] +function Deque:getCount() + return self._last - self._first + 1 +end + +--[=[ +@m pushLeft +@p obj * +@r nil +@d Adds a value of any type to the left side of the deque. +]=] +function Deque:pushLeft(obj) + self._first = self._first - 1 + self._objects[self._first] = obj +end + +--[=[ +@m pushRight +@p obj * +@r nil +@d Adds a value of any type to the right side of the deque. +]=] +function Deque:pushRight(obj) + self._last = self._last + 1 + self._objects[self._last] = obj +end + +--[=[ +@m popLeft +@r * +@d Removes and returns a value from the left side of the deque. +]=] +function Deque:popLeft() + if self._first > self._last then return nil end + local obj = self._objects[self._first] + self._objects[self._first] = nil + self._first = self._first + 1 + return obj +end + +--[=[ +@m popRight +@r * +@d Removes and returns a value from the right side of the deque. +]=] +function Deque:popRight() + if self._first > self._last then return nil end + local obj = self._objects[self._last] + self._objects[self._last] = nil + self._last = self._last - 1 + return obj +end + +--[=[ +@m peekLeft +@r * +@d Returns the value at the left side of the deque without removing it. +]=] +function Deque:peekLeft() + return self._objects[self._first] +end + +--[=[ +@m peekRight +@r * +@d Returns the value at the right side of the deque without removing it. +]=] +function Deque:peekRight() + return self._objects[self._last] +end + +--[=[ +@m iter +@r function +@d Iterates over the deque from left to right. +]=] +function Deque:iter() + local t = self._objects + local i = self._first - 1 + return function() + i = i + 1 + return t[i] + end +end + +return Deque diff --git a/deps/discordia/libs/utils/Emitter.lua b/deps/discordia/libs/utils/Emitter.lua new file mode 100644 index 0000000..d7a3972 --- /dev/null +++ b/deps/discordia/libs/utils/Emitter.lua @@ -0,0 +1,226 @@ +--[=[ +@c Emitter +@t ui +@mt mem +@d Implements an asynchronous event emitter where callbacks can be subscribed to +specific named events. When events are emitted, the callbacks are called in the +order that they were originally registered. +]=] + +local timer = require('timer') + +local wrap, yield = coroutine.wrap, coroutine.yield +local resume, running = coroutine.resume, coroutine.running +local insert, remove = table.insert, table.remove +local setTimeout, clearTimeout = timer.setTimeout, timer.clearTimeout + +local Emitter = require('class')('Emitter') + +function Emitter:__init() + self._listeners = {} +end + +local function new(self, name, listener) + local listeners = self._listeners[name] + if not listeners then + listeners = {} + self._listeners[name] = listeners + end + insert(listeners, listener) + return listener.fn +end + +--[=[ +@m on +@p name string +@p fn function +@r function +@d Subscribes a callback to be called every time the named event is emitted. +Callbacks registered with this method will automatically be wrapped as a new +coroutine when they are called. Returns the original callback for convenience. +]=] +function Emitter:on(name, fn) + return new(self, name, {fn = fn}) +end + +--[=[ +@m once +@p name string +@p fn function +@r function +@d Subscribes a callback to be called only the first time this event is emitted. +Callbacks registered with this method will automatically be wrapped as a new +coroutine when they are called. Returns the original callback for convenience. +]=] +function Emitter:once(name, fn) + return new(self, name, {fn = fn, once = true}) +end + +--[=[ +@m onSync +@p name string +@p fn function +@r function +@d Subscribes a callback to be called every time the named event is emitted. +Callbacks registered with this method are not automatically wrapped as a +coroutine. Returns the original callback for convenience. +]=] +function Emitter:onSync(name, fn) + return new(self, name, {fn = fn, sync = true}) +end + +--[=[ +@m onceSync +@p name string +@p fn function +@r function +@d Subscribes a callback to be called only the first time this event is emitted. +Callbacks registered with this method are not automatically wrapped as a coroutine. +Returns the original callback for convenience. +]=] +function Emitter:onceSync(name, fn) + return new(self, name, {fn = fn, once = true, sync = true}) +end + +--[=[ +@m emit +@p name string +@op ... * +@r nil +@d Emits the named event and a variable number of arguments to pass to the event callbacks. +]=] +function Emitter:emit(name, ...) + local listeners = self._listeners[name] + if not listeners then return end + for i = 1, #listeners do + local listener = listeners[i] + if listener then + local fn = listener.fn + if listener.once then + listeners[i] = false + end + if listener.sync then + fn(...) + else + wrap(fn)(...) + end + end + end + if listeners._removed then + for i = #listeners, 1, -1 do + if not listeners[i] then + remove(listeners, i) + end + end + if #listeners == 0 then + self._listeners[name] = nil + end + listeners._removed = nil + end +end + +--[=[ +@m getListeners +@p name string +@r function +@d Returns an iterator for all callbacks registered to the named event. +]=] +function Emitter:getListeners(name) + local listeners = self._listeners[name] + if not listeners then return function() end end + local i = 0 + return function() + while i < #listeners do + i = i + 1 + if listeners[i] then + return listeners[i].fn + end + end + end +end + +--[=[ +@m getListenerCount +@p name string +@r number +@d Returns the number of callbacks registered to the named event. +]=] +function Emitter:getListenerCount(name) + local listeners = self._listeners[name] + if not listeners then return 0 end + local n = 0 + for _, listener in ipairs(listeners) do + if listener then + n = n + 1 + end + end + return n +end + +--[=[ +@m removeListener +@p name string +@p fn function +@r nil +@d Unregisters all instances of the callback from the named event. +]=] +function Emitter:removeListener(name, fn) + local listeners = self._listeners[name] + if not listeners then return end + for i, listener in ipairs(listeners) do + if listener and listener.fn == fn then + listeners[i] = false + end + end + listeners._removed = true +end + +--[=[ +@m removeAllListeners +@p name string/nil +@r nil +@d Unregisters all callbacks for the emitter. If a name is passed, then only +callbacks for that specific event are unregistered. +]=] +function Emitter:removeAllListeners(name) + if name then + self._listeners[name] = nil + else + for k in pairs(self._listeners) do + self._listeners[k] = nil + end + end +end + +--[=[ +@m waitFor +@p name string +@op timeout number +@op predicate function +@r boolean +@r ... +@d When called inside of a coroutine, this will yield the coroutine until the +named event is emitted. If a timeout (in milliseconds) is provided, the function +will return after the time expires, regardless of whether the event is emitted, +and `false` will be returned; otherwise, `true` is returned. If a predicate is +provided, events that do not pass the predicate will be ignored. +]=] +function Emitter:waitFor(name, timeout, predicate) + local thread = running() + local fn + fn = self:onSync(name, function(...) + if predicate and not predicate(...) then return end + if timeout then + clearTimeout(timeout) + end + self:removeListener(name, fn) + return assert(resume(thread, true, ...)) + end) + timeout = timeout and setTimeout(timeout, function() + self:removeListener(name, fn) + return assert(resume(thread, false)) + end) + return yield() +end + +return Emitter diff --git a/deps/discordia/libs/utils/Logger.lua b/deps/discordia/libs/utils/Logger.lua new file mode 100644 index 0000000..cc582ec --- /dev/null +++ b/deps/discordia/libs/utils/Logger.lua @@ -0,0 +1,82 @@ +--[=[ +@c Logger +@t ui +@mt mem +@p level number +@p dateTime string +@op file string +@d Used to log formatted messages to stdout (the console) or to a file. +The `dateTime` argument should be a format string that is accepted by `os.date`. +The file argument should be a relative or absolute file path or `nil` if no log +file is desired. See the `logLevel` enumeration for acceptable log level values. +]=] + +local fs = require('fs') + +local date = os.date +local format = string.format +local stdout = _G.process.stdout.handle +local openSync, writeSync = fs.openSync, fs.writeSync + +-- local BLACK = 30 +local RED = 31 +local GREEN = 32 +local YELLOW = 33 +-- local BLUE = 34 +-- local MAGENTA = 35 +local CYAN = 36 +-- local WHITE = 37 + +local config = { + {'[ERROR] ', RED}, + {'[WARNING]', YELLOW}, + {'[INFO] ', GREEN}, + {'[DEBUG] ', CYAN}, +} + +do -- parse config + local bold = 1 + for _, v in ipairs(config) do + v[2] = format('\27[%i;%im%s\27[0m', bold, v[2], v[1]) + end +end + +local Logger = require('class')('Logger') + +function Logger:__init(level, dateTime, file) + self._level = level + self._dateTime = dateTime + self._file = file and openSync(file, 'a') +end + +--[=[ +@m log +@p level number +@p msg string +@p ... * +@r string +@d If the provided level is less than or equal to the log level set on +initialization, this logs a message to stdout as defined by Luvit's `process` +module and to a file if one was provided on initialization. The `msg, ...` pair +is formatted according to `string.format` and returned if the message is logged. +]=] +function Logger:log(level, msg, ...) + + if self._level < level then return end + + local tag = config[level] + if not tag then return end + + msg = format(msg, ...) + + local d = date(self._dateTime) + if self._file then + writeSync(self._file, -1, format('%s | %s | %s\n', d, tag[1], msg)) + end + stdout:write(format('%s | %s | %s\n', d, tag[2], msg)) + + return msg + +end + +return Logger diff --git a/deps/discordia/libs/utils/Mutex.lua b/deps/discordia/libs/utils/Mutex.lua new file mode 100644 index 0000000..fd57580 --- /dev/null +++ b/deps/discordia/libs/utils/Mutex.lua @@ -0,0 +1,68 @@ +--[=[ +@c Mutex +@t ui +@mt mem +@d Mutual exclusion class used to control Lua coroutine execution order. +]=] + +local Deque = require('./Deque') +local timer = require('timer') + +local yield = coroutine.yield +local resume = coroutine.resume +local running = coroutine.running +local setTimeout = timer.setTimeout + +local Mutex = require('class')('Mutex', Deque) + +function Mutex:__init() + Deque.__init(self) + self._active = false +end + +--[=[ +@m lock +@op prepend boolean +@r nil +@d If the mutex is not active (if a coroutine is not queued), this will activate +the mutex; otherwise, this will yield and queue the current coroutine. +]=] +function Mutex:lock(prepend) + if self._active then + if prepend then + return yield(self:pushLeft(running())) + else + return yield(self:pushRight(running())) + end + else + self._active = true + end +end + +--[=[ +@m unlock +@r nil +@d If the mutex is active (if a coroutine is queued), this will dequeue and +resume the next available coroutine; otherwise, this will deactivate the mutex. +]=] +function Mutex:unlock() + if self:getCount() > 0 then + return assert(resume(self:popLeft())) + else + self._active = false + end +end + +--[=[ +@m unlockAfter +@p delay number +@r uv_timer +@d Asynchronously unlocks the mutex after a specified time in milliseconds. +The relevant `uv_timer` object is returned. +]=] +local unlock = Mutex.unlock +function Mutex:unlockAfter(delay) + return setTimeout(delay, unlock, self) +end + +return Mutex diff --git a/deps/discordia/libs/utils/Permissions.lua b/deps/discordia/libs/utils/Permissions.lua new file mode 100644 index 0000000..812a17c --- /dev/null +++ b/deps/discordia/libs/utils/Permissions.lua @@ -0,0 +1,254 @@ +--[=[ +@c Permissions +@t ui +@mt mem +@d Wrapper for a bitfield that is more specifically used to represent Discord +permissions. See the `permission` enumeration for acceptable permission values. +]=] + +local enums = require('enums') +local Resolver = require('client/Resolver') + +local permission = enums.permission + +local format = string.format +local band, bor, bnot, bxor = bit.band, bit.bor, bit.bnot, bit.bxor +local sort, insert, concat = table.sort, table.insert, table.concat + +local ALL = 0 +for _, value in pairs(permission) do + ALL = bor(ALL, value) +end + +local Permissions, get = require('class')('Permissions') + +function Permissions:__init(value) + self._value = tonumber(value) or 0 +end + +--[=[ +@m __tostring +@r string +@d Defines the behavior of the `tostring` function. Returns a readable list of +permissions stored for convenience of introspection. +]=] +function Permissions:__tostring() + if self._value == 0 then + return 'Permissions: 0 (none)' + else + local a = self:toArray() + sort(a) + return format('Permissions: %i (%s)', self._value, concat(a, ', ')) + end +end + +--[=[ +@m fromMany +@t static +@p ... Permission-Resolvables +@r Permissions +@d Returns a Permissions object with all of the defined permissions. +]=] +function Permissions.fromMany(...) + local ret = Permissions() + ret:enable(...) + return ret +end + +--[=[ +@m all +@t static +@r Permissions +@d Returns a Permissions object with all permissions. +]=] +function Permissions.all() + return Permissions(ALL) +end + +--[=[ +@m __eq +@r boolean +@d Defines the behavior of the `==` operator. Allows permissions to be directly +compared according to their value. +]=] +function Permissions:__eq(other) + return self._value == other._value +end + +local function getPerm(i, ...) + local v = select(i, ...) + local n = Resolver.permission(v) + if not n then + return error('Invalid permission: ' .. tostring(v), 2) + end + return n +end + +--[=[ +@m enable +@p ... Permission-Resolvables +@r nil +@d Enables a specific permission or permissions. See the `permission` enumeration +for acceptable permission values. +]=] +function Permissions:enable(...) + local value = self._value + for i = 1, select('#', ...) do + local perm = getPerm(i, ...) + value = bor(value, perm) + end + self._value = value +end + +--[=[ +@m disable +@p ... Permission-Resolvables +@r nil +@d Disables a specific permission or permissions. See the `permission` enumeration +for acceptable permission values. +]=] +function Permissions:disable(...) + local value = self._value + for i = 1, select('#', ...) do + local perm = getPerm(i, ...) + value = band(value, bnot(perm)) + end + self._value = value +end + +--[=[ +@m has +@p ... Permission-Resolvables +@r boolean +@d Returns whether this set has a specific permission or permissions. See the +`permission` enumeration for acceptable permission values. +]=] +function Permissions:has(...) + local value = self._value + for i = 1, select('#', ...) do + local perm = getPerm(i, ...) + if band(value, perm) == 0 then + return false + end + end + return true +end + +--[=[ +@m enableAll +@r nil +@d Enables all permissions values. +]=] +function Permissions:enableAll() + self._value = ALL +end + +--[=[ +@m disableAll +@r nil +@d Disables all permissions values. +]=] +function Permissions:disableAll() + self._value = 0 +end + +--[=[ +@m toHex +@r string +@d Returns the hexadecimal string that represents the permissions value. +]=] +function Permissions:toHex() + return format('0x%08X', self._value) +end + +--[=[ +@m toTable +@r table +@d Returns a table that represents the permissions value, where the keys are the +permission names and the values are `true` or `false`. +]=] +function Permissions:toTable() + local ret = {} + local value = self._value + for k, v in pairs(permission) do + ret[k] = band(value, v) > 0 + end + return ret +end + +--[=[ +@m toArray +@r table +@d Returns an array of the names of the permissions that this objects represents. +]=] +function Permissions:toArray() + local ret = {} + local value = self._value + for k, v in pairs(permission) do + if band(value, v) > 0 then + insert(ret, k) + end + end + return ret +end + +--[=[ +@m union +@p other Permissions +@r Permissions +@d Returns a new Permissions object that contains the permissions that are in +either `self` or `other` (bitwise OR). +]=] +function Permissions:union(other) + return Permissions(bor(self._value, other._value)) +end + +--[=[ +@m intersection +@p other Permissions +@r Permissions +@d Returns a new Permissions object that contains the permissions that are in +both `self` and `other` (bitwise AND). +]=] +function Permissions:intersection(other) -- in both + return Permissions(band(self._value, other._value)) +end + +--[=[ +@m name +@p other Permissions +@r Permissions +@d Returns a new Permissions object that contains the permissions that are not +in `self` or `other` (bitwise XOR). +]=] +function Permissions:difference(other) -- not in both + return Permissions(bxor(self._value, other._value)) +end + +--[=[ +@m complement +@p other Permissions +@r Permissions +@d Returns a new Permissions object that contains the permissions that are not +in `self`, but are in `other` (or the set of all permissions if omitted). +]=] +function Permissions:complement(other) -- in other not in self + local value = other and other._value or ALL + return Permissions(band(bnot(self._value), value)) +end + +--[=[ +@m copy +@r Permissions +@d Returns a new copy of the original permissions object. +]=] +function Permissions:copy() + return Permissions(self._value) +end + +--[=[@p value number The raw decimal value that represents the permissions value.]=] +function get.value(self) + return self._value +end + +return Permissions diff --git a/deps/discordia/libs/utils/Stopwatch.lua b/deps/discordia/libs/utils/Stopwatch.lua new file mode 100644 index 0000000..96715ba --- /dev/null +++ b/deps/discordia/libs/utils/Stopwatch.lua @@ -0,0 +1,85 @@ +--[=[ +@c Stopwatch +@t ui +@mt mem +@d Used to measure an elapsed period of time. If a truthy value is passed as an +argument, then the stopwatch will initialize in an idle state; otherwise, it will +initialize in an active state. Although nanosecond precision is available, Lua +can only reliably provide microsecond accuracy due to the lack of native 64-bit +integer support. Generally, milliseconds should be sufficient here. +]=] + +local hrtime = require('uv').hrtime +local constants = require('constants') +local Time = require('utils/Time') + +local format = string.format + +local MS_PER_NS = 1 / (constants.NS_PER_US * constants.US_PER_MS) + +local Stopwatch, get = require('class')('Stopwatch') + +function Stopwatch:__init(stopped) + local t = hrtime() + self._initial = t + self._final = stopped and t or nil +end + +--[=[ +@m __tostring +@r string +@d Defines the behavior of the `tostring` function. Returns a string that +represents the elapsed milliseconds for convenience of introspection. +]=] +function Stopwatch:__tostring() + return format('Stopwatch: %s ms', self.milliseconds) +end + +--[=[ +@m stop +@r nil +@d Effectively stops the stopwatch. +]=] +function Stopwatch:stop() + if self._final then return end + self._final = hrtime() +end + +--[=[ +@m start +@r nil +@d Effectively starts the stopwatch. +]=] +function Stopwatch:start() + if not self._final then return end + self._initial = self._initial + hrtime() - self._final + self._final = nil +end + +--[=[ +@m reset +@r nil +@d Effectively resets the stopwatch. +]=] +function Stopwatch:reset() + self._initial = self._final or hrtime() +end + +--[=[ +@m getTime +@r Time +@d Returns a new Time object that represents the currently elapsed time. This is +useful for "catching" the current time and comparing its many forms as required. +]=] +function Stopwatch:getTime() + return Time(self.milliseconds) +end + +--[=[@p milliseconds number The total number of elapsed milliseconds. If the +stopwatch is running, this will naturally be different each time that it is accessed.]=] +function get.milliseconds(self) + local ns = (self._final or hrtime()) - self._initial + return ns * MS_PER_NS +end + +return Stopwatch diff --git a/deps/discordia/libs/utils/Time.lua b/deps/discordia/libs/utils/Time.lua new file mode 100644 index 0000000..08efd31 --- /dev/null +++ b/deps/discordia/libs/utils/Time.lua @@ -0,0 +1,277 @@ +--[=[ +@c Time +@t ui +@mt mem +@d Represents a length of time and provides utilities for converting to and from +different formats. Supported units are: weeks, days, hours, minutes, seconds, +and milliseconds. +]=] + +local class = require('class') +local constants = require('constants') + +local MS_PER_S = constants.MS_PER_S +local MS_PER_MIN = MS_PER_S * constants.S_PER_MIN +local MS_PER_HOUR = MS_PER_MIN * constants.MIN_PER_HOUR +local MS_PER_DAY = MS_PER_HOUR * constants.HOUR_PER_DAY +local MS_PER_WEEK = MS_PER_DAY * constants.DAY_PER_WEEK + +local insert, concat = table.insert, table.concat +local modf, fmod = math.modf, math.fmod +local isInstance = class.isInstance + +local function decompose(value, mult) + return modf(value / mult), fmod(value, mult) +end + +local units = { + {'weeks', MS_PER_WEEK}, + {'days', MS_PER_DAY}, + {'hours', MS_PER_HOUR}, + {'minutes', MS_PER_MIN}, + {'seconds', MS_PER_S}, + {'milliseconds', 1}, +} + +local Time = class('Time') + +local function check(self, other) + if not isInstance(self, Time) or not isInstance(other, Time) then + return error('Cannot perform operation with non-Time object', 2) + end +end + +function Time:__init(value) + self._value = tonumber(value) or 0 +end + +function Time:__tostring() + return 'Time: ' .. self:toString() +end + +--[=[ +@m toString +@r string +@d Returns a human-readable string built from the set of normalized time values +that the object represents. +]=] +function Time:toString() + local ret = {} + local ms = self:toMilliseconds() + for _, unit in ipairs(units) do + local n + n, ms = decompose(ms, unit[2]) + if n == 1 then + insert(ret, n .. ' ' .. unit[1]:sub(1, -2)) + elseif n > 0 then + insert(ret, n .. ' ' .. unit[1]) + end + end + return #ret > 0 and concat(ret, ', ') or '0 milliseconds' +end + +function Time:__eq(other) check(self, other) + return self._value == other._value +end + +function Time:__lt(other) check(self, other) + return self._value < other._value +end + +function Time:__le(other) check(self, other) + return self._value <= other._value +end + +function Time:__add(other) check(self, other) + return Time(self._value + other._value) +end + +function Time:__sub(other) check(self, other) + return Time(self._value - other._value) +end + +function Time:__mul(other) + if not isInstance(self, Time) then + self, other = other, self + end + other = tonumber(other) + if other then + return Time(self._value * other) + else + return error('Cannot perform operation with non-numeric object') + end +end + +function Time:__div(other) + if not isInstance(self, Time) then + return error('Division with Time is not commutative') + end + other = tonumber(other) + if other then + return Time(self._value / other) + else + return error('Cannot perform operation with non-numeric object') + end +end + +--[=[ +@m fromWeeks +@t static +@p t number +@r Time +@d Constructs a new Time object from a value interpreted as weeks, where a week +is equal to 7 days. +]=] +function Time.fromWeeks(t) + return Time(t * MS_PER_WEEK) +end + +--[=[ +@m fromDays +@t static +@p t number +@r Time +@d Constructs a new Time object from a value interpreted as days, where a day is +equal to 24 hours. +]=] +function Time.fromDays(t) + return Time(t * MS_PER_DAY) +end + +--[=[ +@m fromHours +@t static +@p t number +@r Time +@d Constructs a new Time object from a value interpreted as hours, where an hour is +equal to 60 minutes. +]=] +function Time.fromHours(t) + return Time(t * MS_PER_HOUR) +end + +--[=[ +@m fromMinutes +@t static +@p t number +@r Time +@d Constructs a new Time object from a value interpreted as minutes, where a minute +is equal to 60 seconds. +]=] +function Time.fromMinutes(t) + return Time(t * MS_PER_MIN) +end + +--[=[ +@m fromSeconds +@t static +@p t number +@r Time +@d Constructs a new Time object from a value interpreted as seconds, where a second +is equal to 1000 milliseconds. +]=] +function Time.fromSeconds(t) + return Time(t * MS_PER_S) +end + +--[=[ +@m fromMilliseconds +@t static +@p t number +@r Time +@d Constructs a new Time object from a value interpreted as milliseconds, the base +unit represented. +]=] +function Time.fromMilliseconds(t) + return Time(t) +end + +--[=[ +@m fromTable +@t static +@p t table +@r Time +@d Constructs a new Time object from a table of time values where the keys are +defined in the constructors above (eg: `weeks`, `days`, `hours`). +]=] +function Time.fromTable(t) + local n = 0 + for _, v in ipairs(units) do + local m = tonumber(t[v[1]]) + if m then + n = n + m * v[2] + end + end + return Time(n) +end + +--[=[ +@m toWeeks +@r number +@d Returns the total number of weeks that the time object represents. +]=] +function Time:toWeeks() + return self:toMilliseconds() / MS_PER_WEEK +end + +--[=[ +@m toDays +@r number +@d Returns the total number of days that the time object represents. +]=] +function Time:toDays() + return self:toMilliseconds() / MS_PER_DAY +end + +--[=[ +@m toHours +@r number +@d Returns the total number of hours that the time object represents. +]=] +function Time:toHours() + return self:toMilliseconds() / MS_PER_HOUR +end + +--[=[ +@m toMinutes +@r number +@d Returns the total number of minutes that the time object represents. +]=] +function Time:toMinutes() + return self:toMilliseconds() / MS_PER_MIN +end + +--[=[ +@m toSeconds +@r number +@d Returns the total number of seconds that the time object represents. +]=] +function Time:toSeconds() + return self:toMilliseconds() / MS_PER_S +end + +--[=[ +@m toMilliseconds +@r number +@d Returns the total number of milliseconds that the time object represents. +]=] +function Time:toMilliseconds() + return self._value +end + +--[=[ +@m toTable +@r number +@d Returns a table of normalized time values that represent the time object in +a more accessible form. +]=] +function Time:toTable() + local ret = {} + local ms = self:toMilliseconds() + for _, unit in ipairs(units) do + ret[unit[1]], ms = decompose(ms, unit[2]) + end + return ret +end + +return Time diff --git a/deps/discordia/libs/voice/VoiceConnection.lua b/deps/discordia/libs/voice/VoiceConnection.lua new file mode 100644 index 0000000..d1fd744 --- /dev/null +++ b/deps/discordia/libs/voice/VoiceConnection.lua @@ -0,0 +1,432 @@ +--[=[ +@c VoiceConnection +@d Represents a connection to a Discord voice server. +]=] + +local PCMString = require('voice/streams/PCMString') +local PCMStream = require('voice/streams/PCMStream') +local PCMGenerator = require('voice/streams/PCMGenerator') +local FFmpegProcess = require('voice/streams/FFmpegProcess') + +local uv = require('uv') +local ffi = require('ffi') +local constants = require('constants') +local opus = require('voice/opus') +local sodium = require('voice/sodium') + +local CHANNELS = 2 +local SAMPLE_RATE = 48000 -- Hz +local FRAME_DURATION = 20 -- ms +local COMPLEXITY = 5 + +local MIN_BITRATE = 8000 -- bps +local MAX_BITRATE = 128000 -- bps +local MIN_COMPLEXITY = 0 +local MAX_COMPLEXITY = 10 + +local MAX_SEQUENCE = 0xFFFF +local MAX_TIMESTAMP = 0xFFFFFFFF + +local HEADER_FMT = '>BBI2I4I4' +local PADDING = string.rep('\0', 12) + +local MS_PER_NS = 1 / (constants.NS_PER_US * constants.US_PER_MS) +local MS_PER_S = constants.MS_PER_S + +local max = math.max +local hrtime = uv.hrtime +local ffi_string = ffi.string +local pack = string.pack -- luacheck: ignore +local format = string.format +local insert = table.insert +local running, resume, yield = coroutine.running, coroutine.resume, coroutine.yield + +-- timer.sleep is redefined here to avoid a memory leak in the luvit module +local function sleep(delay) + local thread = running() + local t = uv.new_timer() + t:start(delay, 0, function() + t:stop() + t:close() + return assert(resume(thread)) + end) + return yield() +end + +local function asyncResume(thread) + local t = uv.new_timer() + t:start(0, 0, function() + t:stop() + t:close() + return assert(resume(thread)) + end) +end + +local function check(n, mn, mx) + if not tonumber(n) or n < mn or n > mx then + return error(format('Value must be a number between %s and %s', mn, mx), 2) + end + return n +end + +local VoiceConnection, get = require('class')('VoiceConnection') + +function VoiceConnection:__init(channel) + self._channel = channel + self._pending = {} +end + +function VoiceConnection:_prepare(key, socket) + + self._key = sodium.key(key) + self._socket = socket + self._ip = socket._ip + self._port = socket._port + self._udp = socket._udp + self._ssrc = socket._ssrc + self._mode = socket._mode + self._manager = socket._manager + self._client = socket._client + + self._s = 0 + self._t = 0 + + self._encoder = opus.Encoder(SAMPLE_RATE, CHANNELS) + + self:setBitrate(self._client._options.bitrate) + self:setComplexity(COMPLEXITY) + + self._ready = true + self:_continue(true) + +end + +function VoiceConnection:_await() + local thread = running() + insert(self._pending, thread) + if not self._timeout then + local t = uv.new_timer() + t:start(10000, 0, function() + t:stop() + t:close() + self._timeout = nil + if not self._ready then + local id = self._channel and self._channel._id + return self:_cleanup(format('voice connection for channel %s failed to initialize', id)) + end + end) + self._timeout = t + end + return yield() +end + +function VoiceConnection:_continue(success, err) + local t = self._timeout + if t then + t:stop() + t:close() + self._timeout = nil + end + for i, thread in ipairs(self._pending) do + self._pending[i] = nil + assert(resume(thread, success, err)) + end +end + +function VoiceConnection:_cleanup(err) + self:stopStream() + self._ready = nil + self._channel._parent._connection = nil + self._channel._connection = nil + self:_continue(nil, err or 'connection closed') +end + +--[=[ +@m getBitrate +@t mem +@r nil +@d Returns the bitrate of the interal Opus encoder in bits per second (bps). +]=] +function VoiceConnection:getBitrate() + return self._encoder:get(opus.GET_BITRATE_REQUEST) +end + +--[=[ +@m setBitrate +@t mem +@p bitrate number +@r nil +@d Sets the bitrate of the interal Opus encoder in bits per second (bps). +This should be between 8000 and 128000, inclusive. +]=] +function VoiceConnection:setBitrate(bitrate) + bitrate = check(bitrate, MIN_BITRATE, MAX_BITRATE) + self._encoder:set(opus.SET_BITRATE_REQUEST, bitrate) +end + +--[=[ +@m getComplexity +@t mem +@r number +@d Returns the complexity of the interal Opus encoder. +]=] +function VoiceConnection:getComplexity() + return self._encoder:get(opus.GET_COMPLEXITY_REQUEST) +end + +--[=[ +@m setComplexity +@t mem +@p complexity number +@r nil +@d Sets the complexity of the interal Opus encoder. +This should be between 0 and 10, inclusive. +]=] +function VoiceConnection:setComplexity(complexity) + complexity = check(complexity, MIN_COMPLEXITY, MAX_COMPLEXITY) + self._encoder:set(opus.SET_COMPLEXITY_REQUEST, complexity) +end + +---- debugging +local t0, m0 +local t_sum, m_sum, n = 0, 0, 0 +local function open() -- luacheck: ignore + -- collectgarbage() + m0 = collectgarbage('count') + t0 = hrtime() +end +local function close() -- luacheck: ignore + local dt = (hrtime() - t0) * MS_PER_NS + local dm = collectgarbage('count') - m0 + n = n + 1 + t_sum = t_sum + dt + m_sum = m_sum + dm + print(format('dt: %g | dm: %g | avg dt: %g | avg dm: %g', dt, dm, t_sum / n, m_sum / n)) +end +---- debugging + +function VoiceConnection:_play(stream, duration) + + self:stopStream() + self:_setSpeaking(true) + + duration = tonumber(duration) or math.huge + + local elapsed = 0 + local udp, ip, port = self._udp, self._ip, self._port + local ssrc, key = self._ssrc, self._key + local encoder = self._encoder + + local frame_size = SAMPLE_RATE * FRAME_DURATION / MS_PER_S + local pcm_len = frame_size * CHANNELS + + local start = hrtime() + local reason + + while elapsed < duration do + + local pcm = stream:read(pcm_len) + if not pcm then + reason = 'stream exhausted or errored' + break + end + + local data, len = encoder:encode(pcm, pcm_len, frame_size, pcm_len * 2) + if not data then + reason = 'could not encode audio data' + break + end + + local s, t = self._s, self._t + local header = pack(HEADER_FMT, 0x80, 0x78, s, t, ssrc) + + s = s + 1 + t = t + frame_size + + self._s = s > MAX_SEQUENCE and 0 or s + self._t = t > MAX_TIMESTAMP and 0 or t + + local encrypted, encrypted_len = sodium.encrypt(data, len, header .. PADDING, key) + if not encrypted then + reason = 'could not encrypt audio data' + break + end + + local packet = header .. ffi_string(encrypted, encrypted_len) + udp:send(packet, ip, port) + + elapsed = elapsed + FRAME_DURATION + local delay = elapsed - (hrtime() - start) * MS_PER_NS + sleep(max(delay, 0)) + + if self._paused then + asyncResume(self._paused) + self._paused = running() + local pause = hrtime() + yield() + start = start + hrtime() - pause + asyncResume(self._resumed) + self._resumed = nil + end + + if self._stopped then + reason = 'stream stopped' + break + end + + end + + self:_setSpeaking(false) + + if self._stopped then + asyncResume(self._stopped) + self._stopped = nil + end + + return elapsed, reason + +end + +function VoiceConnection:_setSpeaking(speaking) + self._speaking = speaking + return self._socket:setSpeaking(speaking) +end + +--[=[ +@m playPCM +@t mem +@p source string/function/table/userdata +@op duration number +@r number +@r string +@d Plays PCM data over the established connection. If a duration (in milliseconds) +is provided, the audio stream will automatically stop after that time has elapsed; +otherwise, it will play until the source is exhausted. The returned number is the +time elapsed while streaming and the returned string is a message detailing the +reason why the stream stopped. For more information about acceptable sources, +see the [[voice]] page. +]=] +function VoiceConnection:playPCM(source, duration) + + if not self._ready then + return nil, 'Connection is not ready' + end + + local t = type(source) + + local stream + if t == 'string' then + stream = PCMString(source) + elseif t == 'function' then + stream = PCMGenerator(source) + elseif (t == 'table' or t == 'userdata') and type(source.read) == 'function' then + stream = PCMStream(source) + else + return error('Invalid audio source: ' .. tostring(source)) + end + + return self:_play(stream, duration) + +end + +--[=[ +@m playFFmpeg +@t mem +@p path string +@op duration number +@r number +@r string +@d Plays audio over the established connection using an FFmpeg process, assuming +FFmpeg is properly configured. If a duration (in milliseconds) +is provided, the audio stream will automatically stop after that time has elapsed; +otherwise, it will play until the source is exhausted. The returned number is the +time elapsed while streaming and the returned string is a message detailing the +reason why the stream stopped. For more information about using FFmpeg, +see the [[voice]] page. +]=] +function VoiceConnection:playFFmpeg(path, duration) + + if not self._ready then + return nil, 'Connection is not ready' + end + + local stream = FFmpegProcess(path, SAMPLE_RATE, CHANNELS) + + local elapsed, reason = self:_play(stream, duration) + stream:close() + return elapsed, reason + +end + +--[=[ +@m pauseStream +@t mem +@r nil +@d Temporarily pauses the audio stream for this connection, if one is active. +Like most Discordia methods, this must be called inside of a coroutine, as it +will yield until the stream is actually paused, usually on the next tick. +]=] +function VoiceConnection:pauseStream() + if not self._speaking then return end + if self._paused then return end + self._paused = running() + return yield() +end + +--[=[ +@m resumeStream +@t mem +@r nil +@d Resumes the audio stream for this connection, if one is active and paused. +Like most Discordia methods, this must be called inside of a coroutine, as it +will yield until the stream is actually resumed, usually on the next tick. +]=] +function VoiceConnection:resumeStream() + if not self._speaking then return end + if not self._paused then return end + asyncResume(self._paused) + self._paused = nil + self._resumed = running() + return yield() +end + +--[=[ +@m stopStream +@t mem +@r nil +@d Irreversibly stops the audio stream for this connection, if one is active. +Like most Discordia methods, this must be called inside of a coroutine, as it +will yield until the stream is actually stopped, usually on the next tick. +]=] +function VoiceConnection:stopStream() + if not self._speaking then return end + if self._stopped then return end + self._stopped = running() + self:resumeStream() + return yield() +end + +--[=[ +@m close +@t ws +@r boolean +@d Stops the audio stream for this connection, if one is active, disconnects from +the voice server, and leaves the corresponding voice channel. Like most Discordia +methods, this must be called inside of a coroutine. +]=] +function VoiceConnection:close() + self:stopStream() + if self._socket then + self._socket:disconnect() + end + local guild = self._channel._parent + return self._client._shards[guild.shardId]:updateVoice(guild._id) +end + +--[=[@p channel GuildVoiceChannel/nil The corresponding GuildVoiceChannel for +this connection, if one exists.]=] +function get.channel(self) + return self._channel +end + +return VoiceConnection diff --git a/deps/discordia/libs/voice/VoiceManager.lua b/deps/discordia/libs/voice/VoiceManager.lua new file mode 100644 index 0000000..84ab0a7 --- /dev/null +++ b/deps/discordia/libs/voice/VoiceManager.lua @@ -0,0 +1,33 @@ +local VoiceSocket = require('voice/VoiceSocket') +local Emitter = require('utils/Emitter') + +local opus = require('voice/opus') +local sodium = require('voice/sodium') +local constants = require('constants') + +local wrap = coroutine.wrap +local format = string.format + +local GATEWAY_VERSION_VOICE = constants.GATEWAY_VERSION_VOICE + +local VoiceManager = require('class')('VoiceManager', Emitter) + +function VoiceManager:__init(client) + Emitter.__init(self) + self._client = client +end + +function VoiceManager:_prepareConnection(state, connection) + if not next(opus) then + return self._client:error('Cannot prepare voice connection: libopus not found') + end + if not next(sodium) then + return self._client:error('Cannot prepare voice connection: libsodium not found') + end + local socket = VoiceSocket(state, connection, self) + local url = 'wss://' .. state.endpoint:gsub(':%d*$', '') + local path = format('/?v=%i', GATEWAY_VERSION_VOICE) + return wrap(socket.connect)(socket, url, path) +end + +return VoiceManager diff --git a/deps/discordia/libs/voice/VoiceSocket.lua b/deps/discordia/libs/voice/VoiceSocket.lua new file mode 100644 index 0000000..867de25 --- /dev/null +++ b/deps/discordia/libs/voice/VoiceSocket.lua @@ -0,0 +1,197 @@ +local uv = require('uv') +local class = require('class') +local timer = require('timer') +local enums = require('enums') + +local WebSocket = require('client/WebSocket') + +local logLevel = enums.logLevel +local format = string.format +local setInterval, clearInterval = timer.setInterval, timer.clearInterval +local wrap = coroutine.wrap +local time = os.time +local unpack = string.unpack -- luacheck: ignore + +local ENCRYPTION_MODE = 'xsalsa20_poly1305' +local PADDING = string.rep('\0', 70) + +local IDENTIFY = 0 +local SELECT_PROTOCOL = 1 +local READY = 2 +local HEARTBEAT = 3 +local DESCRIPTION = 4 +local SPEAKING = 5 +local HEARTBEAT_ACK = 6 +local RESUME = 7 +local HELLO = 8 +local RESUMED = 9 + +local function checkMode(modes) + for _, mode in ipairs(modes) do + if mode == ENCRYPTION_MODE then + return mode + end + end +end + +local VoiceSocket = class('VoiceSocket', WebSocket) + +for name in pairs(logLevel) do + VoiceSocket[name] = function(self, fmt, ...) + local client = self._client + return client[name](client, format('Voice : %s', fmt), ...) + end +end + +function VoiceSocket:__init(state, connection, manager) + WebSocket.__init(self, manager) + self._state = state + self._manager = manager + self._client = manager._client + self._connection = connection + self._session_id = state.session_id +end + +function VoiceSocket:handleDisconnect() + -- TODO: reconnecting and resuming + self._connection:_cleanup() +end + +function VoiceSocket:handlePayload(payload) + + local manager = self._manager + + local d = payload.d + local op = payload.op + + self:debug('WebSocket OP %s', op) + + if op == HELLO then + + self:info('Received HELLO') + self:startHeartbeat(d.heartbeat_interval * 0.75) -- NOTE: hotfix for API bug + self:identify() + + elseif op == READY then + + self:info('Received READY') + local mode = checkMode(d.modes) + if mode then + self._mode = mode + self._ssrc = d.ssrc + self:handshake(d.ip, d.port) + else + self:error('No supported encryption mode available') + self:disconnect() + end + + elseif op == RESUMED then + + self:info('Received RESUMED') + + elseif op == DESCRIPTION then + + if d.mode == self._mode then + self._connection:_prepare(d.secret_key, self) + else + self:error('%q encryption mode not available', self._mode) + self:disconnect() + end + + elseif op == HEARTBEAT_ACK then + + manager:emit('heartbeat', nil, self._sw.milliseconds) -- TODO: id + + elseif op == SPEAKING then + + return -- TODO + + elseif op == 12 or op == 13 then + + return -- ignore + + elseif op then + + self:warning('Unhandled WebSocket payload OP %i', op) + + end + +end + +local function loop(self) + return wrap(self.heartbeat)(self) +end + +function VoiceSocket:startHeartbeat(interval) + if self._heartbeat then + clearInterval(self._heartbeat) + end + self._heartbeat = setInterval(interval, loop, self) +end + +function VoiceSocket:stopHeartbeat() + if self._heartbeat then + clearInterval(self._heartbeat) + end + self._heartbeat = nil +end + +function VoiceSocket:heartbeat() + self._sw:reset() + return self:_send(HEARTBEAT, time()) +end + +function VoiceSocket:identify() + local state = self._state + return self:_send(IDENTIFY, { + server_id = state.guild_id, + user_id = state.user_id, + session_id = state.session_id, + token = state.token, + }, true) +end + +function VoiceSocket:resume() + local state = self._state + return self:_send(RESUME, { + server_id = state.guild_id, + session_id = state.session_id, + token = state.token, + }) +end + +function VoiceSocket:handshake(server_ip, server_port) + local udp = uv.new_udp() + self._udp = udp + self._ip = server_ip + self._port = server_port + udp:recv_start(function(err, data) + assert(not err, err) + udp:recv_stop() + local client_ip = unpack('xxxxz', data) + local client_port = unpack('= opus.OK and value or throw(value) +end + +local Encoder = {} +Encoder.__index = Encoder + +function Encoder:__new(sample_rate, channels, app) -- luacheck: ignore self + + app = app or opus.APPLICATION_AUDIO -- TODO: test different appplications + + local err = int_ptr_t() + local state = lib.opus_encoder_create(sample_rate, channels, app, err) + check(err[0]) + + check(lib.opus_encoder_init(state, sample_rate, channels, app)) + + return gc(state, lib.opus_encoder_destroy) + +end + +function Encoder:encode(input, input_len, frame_size, max_data_bytes) + + local pcm = new('opus_int16[?]', input_len, input) + local data = new('unsigned char[?]', max_data_bytes) + + local ret = lib.opus_encode(self, pcm, frame_size, data, max_data_bytes) + + return data, check(ret) + +end + +function Encoder:get(id) + local ret = opus_int32_ptr_t() + lib.opus_encoder_ctl(self, id, ret) + return check(ret[0]) +end + +function Encoder:set(id, value) + if type(value) ~= 'number' then return throw(opus.BAD_ARG) end + local ret = lib.opus_encoder_ctl(self, id, opus_int32_t(value)) + return check(ret) +end + +opus.Encoder = ffi.metatype('OpusEncoder', Encoder) + +local Decoder = {} +Decoder.__index = Decoder + +function Decoder:__new(sample_rate, channels) -- luacheck: ignore self + + local err = int_ptr_t() + local state = lib.opus_decoder_create(sample_rate, channels, err) + check(err[0]) + + check(lib.opus_decoder_init(state, sample_rate, channels)) + + return gc(state, lib.opus_decoder_destroy) + +end + +function Decoder:decode(data, len, frame_size, output_len) + + local pcm = new('opus_int16[?]', output_len) + + local ret = lib.opus_decode(self, data, len, pcm, frame_size, 0) + + return pcm, check(ret) + +end + +function Decoder:get(id) + local ret = opus_int32_ptr_t() + lib.opus_decoder_ctl(self, id, ret) + return check(ret[0]) +end + +function Decoder:set(id, value) + if type(value) ~= 'number' then return throw(opus.BAD_ARG) end + local ret = lib.opus_decoder_ctl(self, id, opus_int32_t(value)) + return check(ret) +end + +opus.Decoder = ffi.metatype('OpusDecoder', Decoder) + +return opus diff --git a/deps/discordia/libs/voice/sodium.lua b/deps/discordia/libs/voice/sodium.lua new file mode 100644 index 0000000..573e996 --- /dev/null +++ b/deps/discordia/libs/voice/sodium.lua @@ -0,0 +1,85 @@ +local ffi = require('ffi') + +local loaded, lib = pcall(ffi.load, 'sodium') +if not loaded then + return nil, lib +end + +local typeof = ffi.typeof +local format = string.format + +ffi.cdef[[ +const char *sodium_version_string(void); +const char *crypto_secretbox_primitive(void); + +size_t crypto_secretbox_keybytes(void); +size_t crypto_secretbox_noncebytes(void); +size_t crypto_secretbox_macbytes(void); +size_t crypto_secretbox_zerobytes(void); + +int crypto_secretbox_easy( + unsigned char *c, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *n, + const unsigned char *k +); + +int crypto_secretbox_open_easy( + unsigned char *m, + const unsigned char *c, + unsigned long long clen, + const unsigned char *n, + const unsigned char *k +); + +void randombytes(unsigned char* const buf, const unsigned long long buf_len); +]] + +local sodium = {} + +local MACBYTES = lib.crypto_secretbox_macbytes() +local NONCEBYTES = lib.crypto_secretbox_noncebytes() +local KEYBYTES = lib.crypto_secretbox_keybytes() + +local key_t = typeof(format('const unsigned char[%i]', tonumber(KEYBYTES))) +local nonce_t = typeof(format('unsigned char[%i] const', tonumber(NONCEBYTES))) +local unsigned_char_array_t = typeof('unsigned char[?]') + +function sodium.key(key) + return key_t(key) +end + +function sodium.nonce() + local nonce = nonce_t() + lib.randombytes(nonce, NONCEBYTES) + return nonce, NONCEBYTES +end + +function sodium.encrypt(decrypted, decrypted_len, nonce, key) + + local encrypted_len = decrypted_len + MACBYTES + local encrypted = unsigned_char_array_t(encrypted_len) + + if lib.crypto_secretbox_easy(encrypted, decrypted, decrypted_len, nonce, key) < 0 then + return error('libsodium encryption failed') + end + + return encrypted, encrypted_len + +end + +function sodium.decrypt(encrypted, encrypted_len, nonce, key) + + local decrypted_len = encrypted_len - MACBYTES + local decrypted = unsigned_char_array_t(decrypted_len) + + if lib.crypto_secretbox_open_easy(decrypted, encrypted, encrypted_len, nonce, key) < 0 then + return error('libsodium decryption failed') + end + + return decrypted, decrypted_len + +end + +return sodium diff --git a/deps/discordia/libs/voice/streams/FFmpegProcess.lua b/deps/discordia/libs/voice/streams/FFmpegProcess.lua new file mode 100644 index 0000000..19e22b6 --- /dev/null +++ b/deps/discordia/libs/voice/streams/FFmpegProcess.lua @@ -0,0 +1,88 @@ +local uv = require('uv') + +local remove = table.remove +local unpack = string.unpack -- luacheck: ignore +local rep = string.rep +local yield, resume, running = coroutine.yield, coroutine.resume, coroutine.running + +local function onExit() end + +local fmt = setmetatable({}, { + __index = function(self, n) + self[n] = '<' .. rep('i2', n) + return self[n] + end +}) + +local FFmpegProcess = require('class')('FFmpegProcess') + +function FFmpegProcess:__init(path, rate, channels) + + local stdout = uv.new_pipe(false) + + self._child = assert(uv.spawn('ffmpeg', { + args = {'-i', path, '-ar', rate, '-ac', channels, '-f', 's16le', 'pipe:1', '-loglevel', 'warning'}, + stdio = {0, stdout, 2}, + }, onExit), 'ffmpeg could not be started, is it installed and on your executable path?') + + local buffer + local thread = running() + stdout:read_start(function(err, chunk) + if err or not chunk then + self:close() + else + buffer = chunk + end + stdout:read_stop() + return assert(resume(thread)) + end) + + self._buffer = buffer or '' + self._stdout = stdout + + yield() + +end + +function FFmpegProcess:read(n) + + local buffer = self._buffer + local stdout = self._stdout + local bytes = n * 2 + + if not self._closed and #buffer < bytes then + + local thread = running() + stdout:read_start(function(err, chunk) + if err or not chunk then + self:close() + elseif #chunk > 0 then + buffer = buffer .. chunk + end + if #buffer >= bytes or self._closed then + stdout:read_stop() + return assert(resume(thread)) + end + end) + yield() + + end + + if #buffer >= bytes then + self._buffer = buffer:sub(bytes + 1) + local pcm = {unpack(fmt[n], buffer)} + remove(pcm) + return pcm + end + +end + +function FFmpegProcess:close() + self._closed = true + self._child:kill() + if not self._stdout:is_closing() then + self._stdout:close() + end +end + +return FFmpegProcess diff --git a/deps/discordia/libs/voice/streams/PCMGenerator.lua b/deps/discordia/libs/voice/streams/PCMGenerator.lua new file mode 100644 index 0000000..cbbede3 --- /dev/null +++ b/deps/discordia/libs/voice/streams/PCMGenerator.lua @@ -0,0 +1,18 @@ +local PCMGenerator = require('class')('PCMGenerator') + +function PCMGenerator:__init(fn) + self._fn = fn +end + +function PCMGenerator:read(n) + local pcm = {} + local fn = self._fn + for i = 1, n, 2 do + local left, right = fn() + pcm[i] = tonumber(left) or 0 + pcm[i + 1] = tonumber(right) or pcm[i] + end + return pcm +end + +return PCMGenerator diff --git a/deps/discordia/libs/voice/streams/PCMStream.lua b/deps/discordia/libs/voice/streams/PCMStream.lua new file mode 100644 index 0000000..186c2ff --- /dev/null +++ b/deps/discordia/libs/voice/streams/PCMStream.lua @@ -0,0 +1,28 @@ +local remove = table.remove +local unpack = string.unpack -- luacheck: ignore +local rep = string.rep + +local fmt = setmetatable({}, { + __index = function(self, n) + self[n] = '<' .. rep('i2', n) + return self[n] + end +}) + +local PCMStream = require('class')('PCMStream') + +function PCMStream:__init(stream) + self._stream = stream +end + +function PCMStream:read(n) + local m = n * 2 + local str = self._stream:read(m) + if str and #str == m then + local pcm = {unpack(fmt[n], str)} + remove(pcm) + return pcm + end +end + +return PCMStream diff --git a/deps/discordia/libs/voice/streams/PCMString.lua b/deps/discordia/libs/voice/streams/PCMString.lua new file mode 100644 index 0000000..2d6c5ce --- /dev/null +++ b/deps/discordia/libs/voice/streams/PCMString.lua @@ -0,0 +1,28 @@ +local remove = table.remove +local unpack = string.unpack -- luacheck: ignore +local rep = string.rep + +local fmt = setmetatable({}, { + __index = function(self, n) + self[n] = '<' .. rep('i2', n) + return self[n] + end +}) + +local PCMString = require('class')('PCMString') + +function PCMString:__init(str) + self._len = #str + self._str = str +end + +function PCMString:read(n) + local i = self._i or 1 + if i + n * 2 < self._len then + local pcm = {unpack(fmt[n], self._str, i)} + self._i = remove(pcm) + return pcm + end +end + +return PCMString diff --git a/deps/discordia/package.lua b/deps/discordia/package.lua new file mode 100644 index 0000000..c95e2f4 --- /dev/null +++ b/deps/discordia/package.lua @@ -0,0 +1,36 @@ +--[[The MIT License (MIT) + +Copyright (c) 2016-2020 SinisterRectus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.]] + +return { + name = 'SinisterRectus/discordia', + version = '2.8.4', + homepage = 'https://github.com/SinisterRectus/Discordia', + dependencies = { + 'creationix/coro-http@3.1.0', + 'creationix/coro-websocket@3.1.0', + 'luvit/secure-socket@1.2.2', + }, + tags = {'discord', 'api'}, + license = 'MIT', + author = 'Sinister Rectus', + files = {'**.lua'}, +} diff --git a/deps/http-codec.lua b/deps/http-codec.lua new file mode 100644 index 0000000..348358b --- /dev/null +++ b/deps/http-codec.lua @@ -0,0 +1,301 @@ +--[[ + +Copyright 2014-2015 The Luvit Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +--]] + +--[[lit-meta + name = "luvit/http-codec" + version = "3.0.5" + homepage = "https://github.com/luvit/luvit/blob/master/deps/http-codec.lua" + description = "A simple pair of functions for converting between hex and raw strings." + tags = {"codec", "http"} + license = "Apache 2" + author = { name = "Tim Caswell" } +]] + +local sub = string.sub +local gsub = string.gsub +local lower = string.lower +local find = string.find +local format = string.format +local concat = table.concat +local match = string.match + +local STATUS_CODES = { + [100] = 'Continue', + [101] = 'Switching Protocols', + [102] = 'Processing', -- RFC 2518, obsoleted by RFC 4918 + [200] = 'OK', + [201] = 'Created', + [202] = 'Accepted', + [203] = 'Non-Authoritative Information', + [204] = 'No Content', + [205] = 'Reset Content', + [206] = 'Partial Content', + [207] = 'Multi-Status', -- RFC 4918 + [300] = 'Multiple Choices', + [301] = 'Moved Permanently', + [302] = 'Moved Temporarily', + [303] = 'See Other', + [304] = 'Not Modified', + [305] = 'Use Proxy', + [307] = 'Temporary Redirect', + [400] = 'Bad Request', + [401] = 'Unauthorized', + [402] = 'Payment Required', + [403] = 'Forbidden', + [404] = 'Not Found', + [405] = 'Method Not Allowed', + [406] = 'Not Acceptable', + [407] = 'Proxy Authentication Required', + [408] = 'Request Time-out', + [409] = 'Conflict', + [410] = 'Gone', + [411] = 'Length Required', + [412] = 'Precondition Failed', + [413] = 'Request Entity Too Large', + [414] = 'Request-URI Too Large', + [415] = 'Unsupported Media Type', + [416] = 'Requested Range Not Satisfiable', + [417] = 'Expectation Failed', + [418] = "I'm a teapot", -- RFC 2324 + [422] = 'Unprocessable Entity', -- RFC 4918 + [423] = 'Locked', -- RFC 4918 + [424] = 'Failed Dependency', -- RFC 4918 + [425] = 'Unordered Collection', -- RFC 4918 + [426] = 'Upgrade Required', -- RFC 2817 + [428] = 'Precondition Required', -- RFC 6585 + [429] = 'Too Many Requests', -- RFC 6585 + [431] = 'Request Header Fields Too Large', -- RFC 6585 + [500] = 'Internal Server Error', + [501] = 'Not Implemented', + [502] = 'Bad Gateway', + [503] = 'Service Unavailable', + [504] = 'Gateway Time-out', + [505] = 'HTTP Version not supported', + [506] = 'Variant Also Negotiates', -- RFC 2295 + [507] = 'Insufficient Storage', -- RFC 4918 + [509] = 'Bandwidth Limit Exceeded', + [510] = 'Not Extended', -- RFC 2774 + [511] = 'Network Authentication Required' -- RFC 6585 +} + +local function encoder() + + local mode + local encodeHead, encodeRaw, encodeChunked + + function encodeHead(item) + if not item or item == "" then + return item + elseif not (type(item) == "table") then + error("expected a table but got a " .. type(item) .. " when encoding data") + end + local head, chunkedEncoding + local version = item.version or 1.1 + if item.method then + local path = item.path + assert(path and #path > 0, "expected non-empty path") + head = { item.method .. ' ' .. item.path .. ' HTTP/' .. version .. '\r\n' } + else + local reason = item.reason or STATUS_CODES[item.code] + head = { 'HTTP/' .. version .. ' ' .. item.code .. ' ' .. reason .. '\r\n' } + end + for i = 1, #item do + local key, value = unpack(item[i]) + local lowerKey = lower(key) + if lowerKey == "transfer-encoding" then + chunkedEncoding = lower(value) == "chunked" + end + value = gsub(tostring(value), "[\r\n]+", " ") + head[#head + 1] = key .. ': ' .. tostring(value) .. '\r\n' + end + head[#head + 1] = '\r\n' + + mode = chunkedEncoding and encodeChunked or encodeRaw + return concat(head) + end + + function encodeRaw(item) + if type(item) ~= "string" then + mode = encodeHead + return encodeHead(item) + end + return item + end + + function encodeChunked(item) + if type(item) ~= "string" then + mode = encodeHead + local extra = encodeHead(item) + if extra then + return "0\r\n\r\n" .. extra + else + return "0\r\n\r\n" + end + end + if #item == 0 then + mode = encodeHead + end + return format("%x", #item) .. "\r\n" .. item .. "\r\n" + end + + mode = encodeHead + return function (item) + return mode(item) + end +end + +local function decoder() + + -- This decoder is somewhat stateful with 5 different parsing states. + local decodeHead, decodeEmpty, decodeRaw, decodeChunked, decodeCounted + local mode -- state variable that points to various decoders + local bytesLeft -- For counted decoder + + -- This state is for decoding the status line and headers. + function decodeHead(chunk, index) + if not chunk or index > #chunk then return end + + local _, last = find(chunk, "\r?\n\r?\n", index) + -- First make sure we have all the head before continuing + if not last then + if (#chunk - index) <= 8 * 1024 then return end + -- But protect against evil clients by refusing heads over 8K long. + error("entity too large") + end + + -- Parse the status/request line + local head = {} + local _, offset + local version + _, offset, version, head.code, head.reason = + find(chunk, "^HTTP/(%d%.%d) (%d+) ([^\r\n]*)\r?\n", index) + if offset then + head.code = tonumber(head.code) + else + _, offset, head.method, head.path, version = + find(chunk, "^(%u+) ([^ ]+) HTTP/(%d%.%d)\r?\n", index) + if not offset then + error("expected HTTP data") + end + end + version = tonumber(version) + head.version = version + head.keepAlive = version > 1.0 + + -- We need to inspect some headers to know how to parse the body. + local contentLength + local chunkedEncoding + + -- Parse the header lines + while true do + local key, value + _, offset, key, value = find(chunk, "^([^:\r\n]+): *([^\r\n]*)\r?\n", offset + 1) + if not offset then break end + local lowerKey = lower(key) + + -- Inspect a few headers and remember the values + if lowerKey == "content-length" then + contentLength = tonumber(value) + elseif lowerKey == "transfer-encoding" then + chunkedEncoding = lower(value) == "chunked" + elseif lowerKey == "connection" then + head.keepAlive = lower(value) == "keep-alive" + end + head[#head + 1] = {key, value} + end + + if head.keepAlive and (not (chunkedEncoding or (contentLength and contentLength > 0))) + or (head.method == "GET" or head.method == "HEAD") then + mode = decodeEmpty + elseif chunkedEncoding then + mode = decodeChunked + elseif contentLength then + bytesLeft = contentLength + mode = decodeCounted + elseif not head.keepAlive then + mode = decodeRaw + end + return head, last + 1 + + end + + -- This is used for inserting a single empty string into the output string for known empty bodies + function decodeEmpty(chunk, index) + mode = decodeHead + return "", index + end + + function decodeRaw(chunk, index) + if #chunk < index then return end + return sub(chunk, index) + end + + function decodeChunked(chunk, index) + local len, term + len, term = match(chunk, "^(%x+)(..)", index) + if not len then return end + if term ~= "\r\n" then + -- Wait for full chunk-size\r\n header + if #chunk < 18 then return end + -- But protect against evil clients by refusing chunk-sizes longer than 16 hex digits. + error("chunk-size field too large") + end + index = index + #len + 2 + local offset = index - 1 + local length = tonumber(len, 16) + if #chunk < offset + length + 2 then return end + if length == 0 then + mode = decodeHead + end + assert(sub(chunk, index + length, index + length + 1) == "\r\n") + local piece = sub(chunk, index, index + length - 1) + return piece, index + length + 2 + end + + function decodeCounted(chunk, index) + if bytesLeft == 0 then + mode = decodeEmpty + return mode(chunk, index) + end + local offset = index - 1 + local length = #chunk - offset + -- Make sure we have at least one byte to process + if length == 0 then return end + + -- If there isn't enough data left, emit what we got so far + if length < bytesLeft then + bytesLeft = bytesLeft - length + return sub(chunk, index) + end + + mode = decodeEmpty + return sub(chunk, index, offset + bytesLeft), index + bytesLeft + end + + -- Switch between states by changing which decoder mode points to + mode = decodeHead + return function (chunk, index) + return mode(chunk, index) + end + +end + +return { + encoder = encoder, + decoder = decoder, +} diff --git a/deps/pathjoin.lua b/deps/pathjoin.lua new file mode 100644 index 0000000..ce20f77 --- /dev/null +++ b/deps/pathjoin.lua @@ -0,0 +1,124 @@ +--[[lit-meta + name = "creationix/pathjoin" + description = "The path utilities that used to be part of luvi" + version = "2.0.0" + tags = {"path"} + license = "MIT" + author = { name = "Tim Caswell" } +]] + +local getPrefix, splitPath, joinParts + +local isWindows +if _G.jit then + isWindows = _G.jit.os == "Windows" +else + isWindows = not not package.path:match("\\") +end + +if isWindows then + -- Windows aware path utilities + function getPrefix(path) + return path:match("^%a:\\") or + path:match("^/") or + path:match("^\\+") + end + function splitPath(path) + local parts = {} + for part in string.gmatch(path, '([^/\\]+)') do + table.insert(parts, part) + end + return parts + end + function joinParts(prefix, parts, i, j) + if not prefix then + return table.concat(parts, '/', i, j) + elseif prefix ~= '/' then + return prefix .. table.concat(parts, '\\', i, j) + else + return prefix .. table.concat(parts, '/', i, j) + end + end +else + -- Simple optimized versions for UNIX systems + function getPrefix(path) + return path:match("^/") + end + function splitPath(path) + local parts = {} + for part in string.gmatch(path, '([^/]+)') do + table.insert(parts, part) + end + return parts + end + function joinParts(prefix, parts, i, j) + if prefix then + return prefix .. table.concat(parts, '/', i, j) + end + return table.concat(parts, '/', i, j) + end +end + +local function pathJoin(...) + local inputs = {...} + local l = #inputs + + -- Find the last segment that is an absolute path + -- Or if all are relative, prefix will be nil + local i = l + local prefix + while true do + prefix = getPrefix(inputs[i]) + if prefix or i <= 1 then break end + i = i - 1 + end + + -- If there was one, remove its prefix from its segment + if prefix then + inputs[i] = inputs[i]:sub(#prefix) + end + + -- Split all the paths segments into one large list + local parts = {} + while i <= l do + local sub = splitPath(inputs[i]) + for j = 1, #sub do + parts[#parts + 1] = sub[j] + end + i = i + 1 + end + + -- Evaluate special segments in reverse order. + local skip = 0 + local reversed = {} + for idx = #parts, 1, -1 do + local part = parts[idx] + if part ~= '.' then + if part == '..' then + skip = skip + 1 + elseif skip > 0 then + skip = skip - 1 + else + reversed[#reversed + 1] = part + end + end + end + + -- Reverse the list again to get the correct order + parts = reversed + for idx = 1, #parts / 2 do + local j = #parts - idx + 1 + parts[idx], parts[j] = parts[j], parts[idx] + end + + local path = joinParts(prefix, parts) + return path +end + +return { + isWindows = isWindows, + getPrefix = getPrefix, + splitPath = splitPath, + joinParts = joinParts, + pathJoin = pathJoin, +} diff --git a/deps/resource.lua b/deps/resource.lua new file mode 100644 index 0000000..f8e73cc --- /dev/null +++ b/deps/resource.lua @@ -0,0 +1,88 @@ +--[[ + +Copyright 2014-2016 The Luvit Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +--]] + +--[[lit-meta + name = "luvit/resource" + version = "2.1.0" + license = "Apache 2" + homepage = "https://github.com/luvit/luvit/blob/master/deps/resource.lua" + description = "Utilities for loading relative resources" + dependencies = { + "creationix/pathjoin@2.0.0" + } + tags = {"luvit", "relative", "resource"} +]] + +local pathJoin = require('pathjoin').pathJoin +local bundle = require('luvi').bundle +local uv = require('uv') + +local function getPath() + local caller = debug.getinfo(2, "S").source + if caller:sub(1,1) == "@" then + return caller:sub(2) + elseif caller:sub(1, 7) == "bundle:" then + return caller + end + error("Unknown file path type: " .. caller) +end + +local function getDir() + local caller = debug.getinfo(2, "S").source + if caller:sub(1,1) == "@" then + return pathJoin(caller:sub(2), "..") + elseif caller:sub(1, 7) == "bundle:" then + return "bundle:" .. pathJoin(caller:sub(8), "..") + end + error("Unknown file path type: " .. caller) +end + +local function innerResolve(path, resolveOnly) + local caller = debug.getinfo(2, "S").source + if caller:sub(1,1) == "@" then + path = pathJoin(caller:sub(2), "..", path) + if resolveOnly then return path end + local fd = assert(uv.fs_open(path, "r", 420)) + local stat = assert(uv.fs_fstat(fd)) + local data = assert(uv.fs_read(fd, stat.size, 0)) + uv.fs_close(fd) + return data, path + elseif caller:sub(1, 7) == "bundle:" then + path = pathJoin(caller:sub(8), "..", path) + if resolveOnly then return path end + return bundle.readfile(path), "bundle:" .. path + end +end + +local function resolve(path) + return innerResolve(path, true) +end + +local function load(path) + return innerResolve(path, false) +end + +local function getProp(self, key) + if key == "path" then return getPath() end + if key == "dir" then return getDir() end +end + +return setmetatable({ + resolve = resolve, + load = load, +}, { __index = getProp }) diff --git a/deps/secure-socket/biowrap.lua b/deps/secure-socket/biowrap.lua new file mode 100644 index 0000000..50f9f19 --- /dev/null +++ b/deps/secure-socket/biowrap.lua @@ -0,0 +1,115 @@ +--[[ + +Copyright 2016 The Luvit Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +--]] +local openssl = require('openssl') + +-- writeCipher is called when ssl needs something written on the socket +-- handshakeComplete is called when the handhake is complete and it's safe +-- onPlain is called when plaintext comes out. +return function (ctx, isServer, socket, handshakeComplete, servername) + + local bin, bout = openssl.bio.mem(8192), openssl.bio.mem(8192) + local ssl = ctx:ssl(bin, bout, isServer) + + if not isServer and servername then + ssl:set('hostname', servername) + end + + local ssocket = {tls=true} + local onPlain + + local function flush(callback) + local chunks = {} + local i = 0 + while bout:pending() > 0 do + i = i + 1 + chunks[i] = bout:read() + end + if i == 0 then + if callback then callback() end + return true + end + return socket:write(chunks, callback) + end + + local function handshake(callback) + if ssl:handshake() then + local success, result = ssl:getpeerverification() + socket:read_stop() + if not success and result then + handshakeComplete("Error verifying peer: " .. result[1].error_string) + end + handshakeComplete(nil, ssocket) + end + return flush(callback) + end + + local function onCipher(err, data) + if not onPlain then + if err or not data then + return handshakeComplete(err or "Peer aborted the SSL handshake", data) + end + bin:write(data) + return handshake() + end + if err or not data then + return onPlain(err, data) + end + bin:write(data) + while true do + local plain = ssl:read() + if not plain then break end + onPlain(nil, plain) + end + end + + -- When requested to start reading, start the real socket and setup + -- onPlain handler + function ssocket.read_start(_, onRead) + onPlain = onRead + return socket:read_start(onCipher) + end + + -- When requested to write plain data, encrypt it and write to socket + function ssocket.write(_, plain, callback) + ssl:write(plain) + return flush(callback) + end + + function ssocket.shutdown(_, ...) + return socket:shutdown(...) + end + function ssocket.read_stop(_, ...) + return socket:read_stop(...) + end + function ssocket.is_closing(_, ...) + return socket:is_closing(...) + end + function ssocket.close(_, ...) + return socket:close(...) + end + function ssocket.unref(_, ...) + return socket:unref(...) + end + function ssocket.ref(_, ...) + return socket:ref(...) + end + + handshake() + socket:read_start(onCipher) + +end diff --git a/deps/secure-socket/context.lua b/deps/secure-socket/context.lua new file mode 100644 index 0000000..3c3d244 --- /dev/null +++ b/deps/secure-socket/context.lua @@ -0,0 +1,121 @@ +--[[ + +Copyright 2016 The Luvit Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +--]] +local openssl = require('openssl') + +local loadResource +if type(module) == "table" then + function loadResource(path) + return module:load(path) + end +else + loadResource = require('resource').load +end +local bit = require('bit') + +local DEFAULT_SECUREPROTOCOL +do + local _, _, V = openssl.version() + local isLibreSSL = V:find('^LibreSSL') + + _, _, V = openssl.version(true) + local isTLSv1_3 = not isLibreSSL and V > 0x10100000 + + if isTLSv1_3 then + DEFAULT_SECUREPROTOCOL = 'TLS' + else + DEFAULT_SECUREPROTOCOL = 'SSLv23' + end +end +local DEFAULT_CIPHERS = 'TLS_AES_128_GCM_SHA256:TLS_AES_128_CCM_SHA256:' .. --TLS 1.3 + 'ECDHE-RSA-AES128-SHA256:AES128-GCM-SHA256:' .. --TLS 1.2 + 'RC4:HIGH:!MD5:!aNULL:!EDH' --TLS 1.0 +local DEFAULT_CA_STORE +do + local data = assert(loadResource("./root_ca.dat")) + DEFAULT_CA_STORE = openssl.x509.store:new() + local index = 1 + local dataLength = #data + while index < dataLength do + local len = bit.bor(bit.lshift(data:byte(index), 8), data:byte(index + 1)) + index = index + 2 + local cert = assert(openssl.x509.read(data:sub(index, index + len))) + index = index + len + assert(DEFAULT_CA_STORE:add(cert)) + end +end + +local function returnOne() + return 1 +end + +return function (options) + local ctx = openssl.ssl.ctx_new( + options.protocol or DEFAULT_SECUREPROTOCOL, + options.ciphers or DEFAULT_CIPHERS) + + local key, cert, ca + if options.key then + key = assert(openssl.pkey.read(options.key, true, 'pem')) + end + if options.cert then + cert = {} + for chunk in options.cert:gmatch("%-+BEGIN[^-]+%-+[^-]+%-+END[^-]+%-+") do + cert[#cert + 1] = assert(openssl.x509.read(chunk)) + end + end + if options.ca then + if type(options.ca) == "string" then + ca = { assert(openssl.x509.read(options.ca)) } + elseif type(options.ca) == "table" then + ca = {} + for i = 1, #options.ca do + ca[i] = assert(openssl.x509.read(options.ca[i])) + end + else + error("options.ca must be string or table of strings") + end + end + if key and cert then + local first = table.remove(cert, 1) + assert(ctx:use(key, first)) + if #cert > 0 then + -- TODO: find out if there is a way to not need to duplicate the last cert here + -- as a dummy fill for the root CA cert + assert(ctx:add(cert[#cert], cert)) + end + end + if ca then + local store = openssl.x509.store:new() + for i = 1, #ca do + assert(store:add(ca[i])) + end + ctx:cert_store(store) + elseif DEFAULT_CA_STORE then + ctx:cert_store(DEFAULT_CA_STORE) + end + if not (options.insecure or options.key) then + ctx:verify_mode(openssl.ssl.peer, returnOne) + end + + ctx:options(bit.bor( + openssl.ssl.no_sslv2, + openssl.ssl.no_sslv3, + openssl.ssl.no_compression)) + + return ctx +end diff --git a/deps/secure-socket/init.lua b/deps/secure-socket/init.lua new file mode 100644 index 0000000..ffbfa1d --- /dev/null +++ b/deps/secure-socket/init.lua @@ -0,0 +1,34 @@ +--[[ + +Copyright 2016 The Luvit Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +--]] +local getContext = require('./context') +local bioWrap = require('./biowrap') + +return function (socket, options, callback) + if options == true then options = {} end + local ctx = getContext(options) + local thread + if not callback then + thread = coroutine.running() + end + bioWrap(ctx, options.server, socket, callback or function (err, ssocket) + return assert(coroutine.resume(thread, ssocket, err)) +end, options.servername) + if not callback then + return coroutine.yield() + end +end diff --git a/deps/secure-socket/package.lua b/deps/secure-socket/package.lua new file mode 100644 index 0000000..c5851d6 --- /dev/null +++ b/deps/secure-socket/package.lua @@ -0,0 +1,12 @@ +return { + name = "luvit/secure-socket", + version = "1.2.2", + homepage = "https://github.com/luvit/luvit/blob/master/deps/secure-socket", + description = "Wrapper for luv streams to apply ssl/tls", + dependencies = { + "luvit/resource@2.1.0" + }, + tags = {"ssl", "socket","tls"}, + license = "Apache 2", + author = { name = "Tim Caswell" } +} diff --git a/deps/secure-socket/root_ca.dat b/deps/secure-socket/root_ca.dat new file mode 100644 index 0000000000000000000000000000000000000000..8c9097e2a7d3f28e21492d413a46703e846aa086 GIT binary patch literal 146914 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

C7r`3MzDTH z4Y0BY$6w#Jb~d_()-(G&9Qb%3Im6mDZ`0Ai0nOy|d`9OjS|L(OJ8Wbr3hRL$-;gc0 z24dK(ny{_*?nQm6xjoD=@ihOu4sKWG#McMXKninc+pTp9m#sADoO$l@Y+Qoav)53A zg-pd5*i4_75ok&&lZ;N|3|v3wrmD5<7hBEe+P@vMt$Kl;*}7@9s<~gW|Amo<4>6$b zVaV7U=4~OOhHBHrBNL{;&o&Su%3pbdvJ%(1o+sPGeg=C~!(i@sb;lbpeD%tazYr|Y z?+5A-Rrq1Ghl-nnrm-0~6ZKmK*&l^wcMCfHx^xu80%W?g>i2%X!oM1SOCKO0JBSx- z<-tEUcK)Wx{k{3W|A7BQA+5%BiE6L4F3$EXk@<+`F#XLEYdY3445?wd)gQ zNDHuN*1AyK=f7C=Z|E1$>?{u$wpbQI(xT<|slTi8N+fk6lckSJFwZjx0`Y4(>M-%)!FK1zf^xe!~oZmJflDHzJV~022s_30|;+3jTu#hX}p}4CEVa z2uNVSIWXY!Z})(L0tR#w1$3&KgNq}!?Wrsj^WcNeg@y#V+k_5fo?M{w=5v5BoYzy;iEX6$SR09#Z$@Co*208jH4FxT(cv0+{{;5l3fZQ&+GCc44|5|I>5*z=i;22k>1yS%8$ke=0~kyzhK? zlPv!2y|I}5m`#h6v)lee-v~#z#mif~mlSN%zdHvEsJMA|;0l8RV89E1oWB~A6A+vi z5b(c_vcNlX`c*9c1*=DV#;LnHOnq8N_&LwlnILw;>ai;IjHLswS7(WVz@qj`1;^xI z<2B+h)&yvS=Tgeqir0J85qu}df{2k`6#G3dT5cdlU+ikq@%n?f>r8nipg_3leMezk3rOIOeZqB z(r+L0n#S}P9@TGe7%686JRCz6Fo70qe24gEj;~M+XWpeuX;AtLSrz*h)#bDLfmEeHo*Ttc9`(ir!SQ%wx6Gh8NHidrKVe~`$PZU0H zy<1%I8s?&kkagwy2HON?^#-sS%(`QBC|JZ_IsM0c!N2D8KQey5Kt$wwnCvFo%K_qH zFZT=}pmP*hkmw*bvTnSAdv4c$w z#`^SIN&Hj&%e=|!Oc|Z@74{YC=O;q1bs0&#INy$1UX@xXN7~UDG475Tb#y;#j^>I# z@H?Ht#O_RgPDS5Nge}ki5QE(_+u<-n3;qnz2!Mjo^o( zix-omFF#aKsn3bK+ii=Cw^vivtC7rcl1}G5VgM~MA1yCW^~_%{`;Q@;W?!@Cpuc9q z#ob7%`IuYxWA*wuH%C? z8WhwzDKq`kTIXKLd}sn%jVB&H?iWY%w$o#S4DoxD^TU@g8XKp8r7F&suzOoNY`=WC-M+#Y+HOYr~+ZWG~ig1~H6ueBkY?H+K zKRR^C;v=2LebmY&RLN&f5}AZoDpsD1i%yVS5Zp_+#DL&Ay1yp8P;0MA1Q>A8j^1XG zh#g&jCbU|`G{-ke88sc;MCmYmy41zH4inDb@p@Ck%g#TA`p6dY{50uhg6`(!M~APE ztWY`4sLEbxKcBWCvF2Age;;d&Yq9-=Um)3r;MPjPvd|c)r=Q)i zJ}%utn2U)+XkQ&lY$YhoO%&ds|J)$GdzaP(VKJUi@8egBnb-KDfk{pQYM|n*V;_@N z=YJ`#wr@fP%+1uh2|2$unt`-^rU7$i>0&Zz6AYu;lyKIpqN@Q9SkZmE=#K6n3t*yR*c2WzB2W@WMH!P#?s* z4n2JZBU}z82P$ojj<*^Ipbn}z$2&E9c-!v|LYhFO!(9qHldvWO)i=1uw z1~T`-kkbk}z58{Qrl$lWd8U{8+6#J4t2{_0^gI^sUEY+Wo2fC9? zlln3C{4WG2&jXid_Vub^*&QOd28Xc~uUlnNIOGdDC|Z-uwqM6H`yTdc(JG@meV8^>Q3R4BIf_?^-Ma98@RMhqY8u!%Djve8uX=NLykDz|24&)G!>Z~$&@4>4$PhxJAD>o= zN5gKOP0X8%OnFn5#vZ;~7f?yuMXAj*n?-uy77-sqw;^rq@N^SjLts$E| z%TA9Xysr6(vK3Dg7EzPC;26tZ3SEJ;gzBAB4tl)t(ns3YaeOB zB5RbfrckF|msdjfGw#VvUJGx2cMc%IwUdzXe^&Pa3wPokTuK`-3Jm!4hq8wT2D}3X zJo{Y(gOzixv zBvSdiXa}gdf&*!G=Ju{Ef4DSIka)K)4cMdkQJCK?z`YH0m^!-rTm<}wiP~?&o(~ig zJl|opWP8r3H}$F?YWOD;^;ZM_eI|+>Y@&YUo?8>8RG!Twf7Z6|aAKiRO2x6WNVt9Cw;{3p<@V>s08f|XcF#;vSYHPOhayqpQs4C8jb}AV232nh69iFk)ve1hi z{h(&Jt`CnMqT5lk%6Zuq!awi(5}Z{Y9!Xk?7p-f7KY@1`>Hl%&wA#Jnf(N?8*Iv)SV=7H3)f$%!c};(_ANBsSJ73H z@>Nk3WKr%G-N_mxIpwwQbDhM+aPo`fk^15 z1iVQod{7r?^Crd9t9W0QTJ^;B^0neh5h;{?6{^2@d&MUzZo{UK$Ba)|)_n6LXHphg zX&spLOT5FbkY?mAVvcB*>>Wkz*|$KM-p|^Xdaa2B+8!!W+U=BTD|8Kuv@>ofVTBoN zN{rhcB}0A69@mI~OCik7e)IvDSvKId6Lj>_gm=hRYf=6qxGv0%jySdTl1S`p-=p*U zvBs~g>F`8h3>+CqD8^$KTBvlHU4@@cm=dD8%vd*@NHx#9GFET;yb&R;YmnlB>uDH6r%(^sv#ZPBm-nvxM$pC_pP}$i!+T$W7#HSP)1z^y%DJ= zAsYort{y z`o~AtI}G#d>#udqC{t4%nD}U{RbW$SrbkF@oVrqd$TDUg+jd1+TFMHZI=v*MEk;Bs zNNkmhLOIh&zCXT3nIq@K<5`yU)Vk)0?*@exTr$q{u#d)E&33e!nOZw-ZJL*CLwOT1 zZ>h$jiw?%`l_f1X@*LLl)n?pJq7i$M(=7AAS)K|Q2C03l_=dzv)1vadh<;Cv0)vKR zDfuMXqOic!d!NRq{^{H^D9_C@rYTR%BIk@}aT8Q=vwc*1<%%eJyQ;SPI}(jPpzoDB zp3e0v_->7!l}@zysqg2nd!9{M7+-0`*$P~gX?u!~HJ)pUri`Ij_0R;{cS_GWnL zpV{qC^87FScPruIqNXJ!ea{-$I{X?V?9HyH`tu(?*<78!NwaJ1hP8VD=p@-|m<-m2 zGCh;V+f~X(K9Nn{7XsHVNj)?jM3CB474k^0RH!%|Q5s53X21~PRqdRaNJHCw23b+f z(q|<~Eo%F+hEMSQ`lNIxZvXl>c=V^rG7&SNB*Wfy?vnct%##w>dTX)-`AoFm$&{O* zIpwpJuy#Fh7Zb+DwiL)!zlYKPm2ZFIY)VG6so<(mgtKEl+_*QqdyWf(GWo&AOWAWx zJQw>ig%ipgrE*d)W(nG5V7tNoBzFHjQI$j~=qOu?0awv&wQKgsSadB&Jl3ItonA*u zoR-Tsw#d-X)L?u8!1zM=jxW+*@x}WCz9eNp=-~cae8H(X*qb{W{}!W##>TiiaJRUh z?YBz8n*v?%I(;@?j@vDEery8&8<+kk#Khsta%S}Hp+4^xKf~j`kZx)5^JnI~zu-`0 zS=xwWpJ?x{G9BZGME{8@d-n5uc>JOe86Lw%zY zpEZIIdhLQ}cleP|a#7)Hl6fbmU2UxZWiP#lu99d$(8(P!MfUsdZ1%K4Z(RbYAy|M>zU=;DDy{-$`hXa!BV`&Qr5?u{bj@GwepM?{<{yO z;tjdnPo`>!2{#U_F)xjs#9QfV_()r8#MZJ}r$Go#FEgS?Ii%R)y3DChvcbhaF0s50 z_)*SvEpk0xr`|ywbc*SS89yt@xqDcs=T$FXWD0FI(g z0h9cxTyF|=QEhBf$>Y7hecKg%t*DqefD)ZBJt2{9uqCQ&T5+h~S%o#-cfW`w14jqe ztzm1-bn!K3Jq$wp+GIxhu|*w08B&2d4F~&2;`YyeM0_#GC2aOn7fScCCfK^4>o}Ip z$-{7fog^R_DFk;Y!B9>BC!IG&(iKLMcHLsL%YOw;K;RFk0WZAyUUQ3#je8@_Je-Z~ z99_N}p&Mle0xxH{Llv>MD!31v&irnne#=JP?9&1IV=2wusNc%re+umWzHP=`Dij1x zg@V%;96*j6r;8IjAwb}jG~A$@9W(yF@R0um9MNBLd zqVFfaCnPpx)YYJ5B2?H=`%u!C(==q?c6cww3p0Z`pG7H zX3ckieEk{gqS}P-V}=g*=Z10_<&6LX?I!Gp(Xn9IlMI!Nb@rCN#h0#<-fvzTI(*2? z@pw_%m?Y-FSbV@CTedjirNZQDF^88kPm#y~e*&h@W=}y>Luh{=`~3+4?#Od5!BD1E zmoX%*GP5+o%|mn;gorX|d_uEBoqLqg98A^6R-{;VpCZU`COcyb7O6r;yii+g=GV8j z@S8-5ElcigO9W9|wD&$9F%dl5f5-gk^0WE(cvBV-yjKtQcZVd98wUyojtmAu3_|2b z?1ugs?0+cyE~Q)nIMZOn^g)v)n6hZSy7hYEf4z0jpQb6Rwv?F2ZC(`P_b0)HyxE)U zFPG@Ws)(ji04m^yt?TF3egWj zJe#}hed_Y$iCN|<;}S_c)5pd^pFsyu%BPh#AIakO5OF;9s7e(+PEwF7q+T9zKk#v0lh-Ajib&>({qY_-Q-I`JZ0mLPSLzRRnQ=OSRGkhwX~Z`G%R49vQtF& zVu8PAuNpIb=&|t1%&M4A?l@mvar8b%O6O;c1-MJZV2t}ao$`lUw2wvKx;_q!K7O1vO$@ZUF9m>q1Ae{TZ+BM%V>3HXsC?>4ClZgGuKPImL!MfRyA5q*#?iAw$o(R9sLC~2}}_jCie%1h5Vq@Db+E? zs{=l_xe3VmbwBD5^e82Qt^+(|wPCD!7w+5HME0_MT!Pl2gj@S0jAm?{`M4Orf!vu( zu>fDlJo)JqgS~$4Rroyetlx4p?!sn2-^e0XE;jRcOr;@~KsEv_LcLzTIS-6P3fT)I zBiDB#`OYT9Chy1{42eYFK&hA)RZtPwTzEhiEBN_$oR#Wa&VZmxp2%*92W$=2=?_eK&lp)-6f086# z4<9>UevJPtkLgrn+%J%GHB)5s`3o>0`o?^NlV~Ae{`SZDt1&s2dFKC-Z#57S0*G?+ zIM}~!$oCi8flcn9x=%3v0;?+rJ}Tx>Hai@SUK|<02||GY65+Qne@uJprRVs-YmRtR zXP%t_637>%33Pd&W3Nv2vEyZsHgxX5pvhE%giBqkL@#ySNFY=>m8W&|t9pgOA^ARb zsx^}El8i_Zd%Rx2@{`Q>_a_}sd{bxuIO_LG?Oj4v(kL>JYy;7XPT!)OVUXEA-wi|U zL^qBs-w9$&JdJr8?996f*a+GVdY@#E(7dTgQ}4pK^yygsipFWt7}HhuT8k(Q-A$Th zNW~gI8w$%fapZ$=HHQ37XBmcAL4GFENTNw9>QH##J2sRh#w>{y)GWI6szwGKt#WSk zN>ab6mMj=-FbklAS>WXFSitIM7WiJ-fP_r~j`BELSex6L0o2V+t?V6a9W1SH61KO? zvKb9=B&yi#w;z6Aa$VTr=ESrWHS;D-iOYooCqVsZ?Xl?1P1TMiy&9 z!ds|trc>_%bI)S)k>BcrF4>fOIO>};-3NYzBS6R4%9oN1*X5h^+hbWJFkvq~e|wV8 zVK^lgAILRf!C&>NG4L{-PY|Q${w|!kg;yFOfxxws3O|s=@QuJDmuh`D+YTztRB);L zQ<6fcnk6CBN(9fqS6l)c>7O`jnyvee1~S$z&7$%B+3gKv^;3Gu!fG>!+D7cLIjIch z(hLQ5M^D_}ZU{DcppuAMK7<)R9i4QD+!xP=EWPCeaLDoP4<2u;VZ(>P8S>CWU5&Eo zF^sop#o?>KfT!R2AOaW==8yANV{&ZsZ2og!?SF|BU^9fmGd}bZY2=hy9~?ZGOoI#~ zQqn5U@kC^pI$^CQ8|XH$84n7}N!pd+6nq3HqHWR|qx}lWQfRAYhQflpSN+U+KtNu2 zJfQMXW{_7?Zub!fe#``>@Zkj$g95H!p@}}ry-yIx03uSWZ}_hcTOzmV9?U4H_~ytdPHZ5WJQV^djDB5% zQX7J5;|rD;y{;6(Oq7U?Ib6{)MTtmlB1K}O&j*fD?9uY(=|utF_bq~MjyVJmpnEB6 zW79_r7{>DJKO@RRAE@K|oZkAy-iTLZanMcyZax(_0HKZIf-sBfk3uSy<=A3R>qpOcs3$|Hs(@gviU zi0*jzf#S;~wU9L9c@|4f!YUKVvk$GrkYQ>mgM~*(6z-3%iyOLJ5|kuQFl4yTiXO=5 z+XTRC?nius8$NNQRH$tbX=Hm9tRC9+vAT~0 z%`;%M&d~$`l0{JoZ3MHjxNEBE~Qw}AyAwJ2M8-!wG@WLx}m%bvI8 zRqy?4lcwDv@o?8|#_|?Cqy|Gr&Orxfpf?;Pbmxxpq%z%=Oc{0-1@+NpzI6bbjQaug zvBD6Ra!*2E4VYiys;$Gcj~bGSk0G(rrb_zcpt_Z-hvH7l-2eJ~5Irq>>xGy8n-K;# z(Pi)1{Q8ngy0WskGM67~hmlA0on#4Tp9lnaH+a&LymM5*N8h|(Rb)L03 zAvVG}%wMW^vMeFrSX5{i6~3~R#OGm^5^_qfSf=PRX10Sb3VzJIqjK-7X!v$ z-3ThXs+AU@@(%*fkwc>2KcsDHfKA(KiZ4cj$AU^y1jOxKqmnI(-2WuZw;HWKX|V+L z5?(b~4=sj0{cS<-Wy<=wA(TF-yRW)fR=5BL5rih>so#*zUZ;;woNyf6>E z88j>-8Z;CU7!>euu$Bza5D+jp&}n;=*w=}4T%V>h+t|)Qd=V<XZ9Tvx4YoJVpsDY5GF$d*ryVWMIgvB9El{cQCqVt70~ ztCk#Pmd96%=OTJh$p9s*^!~?dx1|2OFO~?`%WZgd(vUvu-;l0}mu;+DJt&Ia7E%?5 zmbz=Mp`jdO&oEd-n8^qb5roCTWQnRaDD_`SiMVs_UPHKZNwB4LNrvbzQokhoBK5&d zQ-(L=_d|qnugL1nh2F_c`qp`I(!XHwN;aZ=ap*&<$h=YO%D;}D$BvGH*^CW2x{UUz zwGJzHL4vAn0Kwk4;Yn%G7izKn8 zF56q#cL4+-49$|@U1p@~SLBaAw&rVRn=FmV#k)m6@lW=(>sM1Ws64Jt3Q205)PQTG z-WluSAu}b$x}Uh9UA8|LJYtx_imIWz!ZXDpHHgDhB{IqV97w%MXm-ZsnB)?stCJB?g;COKy` zm0s{b*^SED3y!PjLHsMvZzZ1eZ>Lzx~QVnKt;m7u}pN>Ja+ zl`v3N=a=!vC9h(ajV_4yz}tcUtYCrWKkzO%j)M#K!Ed9VDB{+Z);Av4ZD8j6uH?wr z7&qY^GjNwFxa-Hr4m39L?`MElowzxJ_fe4%|4!L=+Zlk1o4)UDdh4TsTfaB|Q;+da z!I*M1+`%O=f4!=(g9f8-PFVRB(=-X;C5MAY0A4z6Ba~DB$zt3@He>#}D!Sl^i0LdO zlpuVs*toKYRZlfNP%y(WdNLcA`@rlBe^KtP%{j%ss_|SFV2pgRYT}B(>?4-0;Wupp z&St6iIb7c!`h9-MBiK&d5yd_+&Ps4ahh9?{G}>V=-e%1C9PjEudZfGE*wl3L!|z&|^B!o~p?uE&NVj6111KPYX0_TB?(_OA7ZI-W3TY;tgNtJl{kxBzUBB z$6+^Xl7B7@{Fi|jiOt-~!>9d$AZC@w!a|CS1G^sdpH|HM<$#~(;qA(kT(su7#qGT> z&MsbN39Tp27UOJ7qHZP?VVnCS6SJB%Sp$PObd9J#2Sec7RQInt5Dn4V;`6s@*k&xYL@L!Y;HHF zvp}DJvW~=N)`E$QWq0U9VvI(75kWv@YZH}MV+j6wD2F-=%`xrOQu}#(vSJNAEL=a+ znnHQO1%fzp_NN2L3IM#N0C*E`gC@9p3u@6K60fr!TJ>tibj$n1=^*1i|9^sFLH--)Qc3dqYz!Ad$aR)G5VirgTs@)1CJ|^2h}K_ zigs9nsi!SPkk1Cg2!Z*+_xBU{x6Hr4=P0o>X4VZ|@rVW|$}^of;qh*bR1VzKSg8jR z4~(ggj4nyzowJwCyR_pjJ|1q#nt>K}Iuxw3E%L1!TDHp$Ev{%Sm*6LXlP|>3G*}BC z;*JhxH%l(jSRFlQJClYp2piF_%b}xnzMMhrM2(wNhjqIKIjy6PS2v!knNYiFJOtC=Co|fYr$RWtX zzW6A^uUF-h6Wv~V%aHaDf);X4fkSOjuEFe6&JqDZdty2Npz0q$`==-T326ILxr(GP zF%T|h#Zn(Al^xBOKpnfg!{t!oUdqx3v@+I>c3&N}k!bYQ+NOHAtS8-liC(qrkeT&f zE+jcFJjP+^B`RTaT;W&a(G^&O;hdH7#0(5np z$^*O#FsDo#9qQ6HhGe*-P_Nwh7)ISmNU)uQ>;*vE34r$1 zEodQ%b*QZng;Jsr-49A6s9b*rtv*Q z9q<3{-TVs5UGG+%*FKLltTHmP=FhMC3iiIIlCE~7YafkgH_t{OEYr8l7kb@MQy%pp zDYsBqlXrL~evEJ0g*#W~-F+&*Q<*$v4DQ`SXfw5Cg_%;25$9v=oL*TVS&I~1l)mgBINm+G{h$s$)U zNDXQgz4Go8jD2^WkZcHaFB-F&kQ%pxw+Jt|Um}>iX))7H2x!ZqA`aHYS3o4icqiOo zhp5P1D|^cn_76h&wLrTic$t2I{%!^uZSsw|Zskc2n^q539jpVpUzZ9=ZpOj;ft&Q0bWr(bB>z`g$c6S_G zC5s22QVo7iX>#(^?A=@{*ZUXjuPsw3deYs<=^aFuRV7&r@%A)21DWMPCz&ib6g}%p zrZ{YUE%*eP{AX|V!dSu#;6fRZV04sfI?fT22C?-sFq*wMjk%k|a(z~)=@B{-Es;0F zms-)(YkMAH#Hb!fSxz+4L1?3jjgYcTAJsch>qZ--H_G0DXRhoc7PaVwrD+?|Io%?w zv%pB@OKnNNe4>+L{C-EWh>FHMlj{)7@3G2w;n^k=W{&Fyi45d30Lrldls&#d867OJ zN%nAJYT5E6eq6sjPmJPcEc5?>WeFt~^qcw=(3=By9XUJLJaqm(912MEe9v9}CI)ch zTzLR$LV)F;>za!QNLR94AO1%9|1VztS7@72c=iF~jAIDS&7)e$;fww5KplFYuIu8? zrZ-f?*gJb?QFVi2HUzO9NPWU5lrgI>V$?AFP8aBprb`T&LeV0uZR#r^nnpQ0rMM`T zK520&vE9K~8Z@}y%q)z)d~$~hDJ-UyAlWln4Y_o8s8LdV* zFXP!Xgt=Wh7Nf%Esf$Uf!tT|GYFd|3=G#VxbvSM5vL)5@&FOjW#qSdcyzM1x>r8?u z%0A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 McU94JO%e=n5<%E| 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%0A0l$wvqR5F;!7T|`E4qp@`<6~F?U+{5UWSZn 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 LKS-L@6XOK{;k%k6 literal 0 HcmV?d00001 diff --git a/deps/sha1.lua b/deps/sha1.lua new file mode 100644 index 0000000..2a65391 --- /dev/null +++ b/deps/sha1.lua @@ -0,0 +1,194 @@ +--[[lit-meta + name = "creationix/sha1" + version = "1.0.3" + homepage = "https://github.com/luvit/lit/blob/master/deps/sha1.lua" + description = "Pure Lua implementation of SHA1 using bitop" + authors = { + "Tim Caswell" + } +]] + +-- http://csrc.nist.gov/groups/ST/toolkit/documents/Examples/SHA_All.pdf + +local bit = require('bit') +local band = bit.band +local bor = bit.bor +local bxor = bit.bxor +local lshift = bit.lshift +local rshift = bit.rshift +local rol = bit.rol +local tobit = bit.tobit +local tohex = bit.tohex + +local byte = string.byte +local concat = table.concat +local floor = table.floor + +local hasFFi, ffi = pcall(require, "ffi") +local newBlock = hasFFi and function () + return ffi.new("uint32_t[80]") +end or function () + local t = {} + for i = 0, 79 do + t[i] = 0 + end + return t +end + +local shared = newBlock() + +local function unsigned(n) + return n < 0 and (n + 0x100000000) or n +end + +local function create(sync) + local h0 = 0x67452301 + local h1 = 0xEFCDAB89 + local h2 = 0x98BADCFE + local h3 = 0x10325476 + local h4 = 0xC3D2E1F0 + -- The first 64 bytes (16 words) is the data chunk + local W = sync and shared or newBlock() + local offset = 0 + local shift = 24 + local totalLength = 0 + + local update, write, processBlock, digest + + -- The user gave us more data. Store it! + function update(chunk) + local length = #chunk + totalLength = totalLength + length * 8 + for i = 1, length do + write(byte(chunk, i)) + end + end + + function write(data) + W[offset] = bor(W[offset], lshift(band(data, 0xff), shift)) + if shift > 0 then + shift = shift - 8 + else + offset = offset + 1 + shift = 24 + end + if offset == 16 then + return processBlock() + end + end + + -- No more data will come, pad the block, process and return the result. + function digest() + -- Pad + write(0x80) + if offset > 14 or (offset == 14 and shift < 24) then + processBlock() + end + offset = 14 + shift = 24 + + -- 64-bit length big-endian + write(0x00) -- numbers this big aren't accurate in lua anyway + write(0x00) -- ..So just hard-code to zero. + write(totalLength > 0xffffffffff and floor(totalLength / 0x10000000000) or 0x00) + write(totalLength > 0xffffffff and floor(totalLength / 0x100000000) or 0x00) + for s = 24, 0, -8 do + write(rshift(totalLength, s)) + end + + -- At this point one last processBlock() should trigger and we can pull out the result. + return concat { + tohex(h0), + tohex(h1), + tohex(h2), + tohex(h3), + tohex(h4) + } + end + + -- We have a full block to process. Let's do it! + function processBlock() + + -- Extend the sixteen 32-bit words into eighty 32-bit words: + for i = 16, 79, 1 do + W[i] = + rol(bxor(W[i - 3], W[i - 8], W[i - 14], W[i - 16]), 1) + end + + -- print("Block Contents:") + -- for i = 0, 15 do + -- print(string.format(" W[%d] = %s", i, tohex(W[i]))) + -- end + -- print() + + -- Initialize hash value for this chunk: + local a = h0 + local b = h1 + local c = h2 + local d = h3 + local e = h4 + local f, k + + -- print(" A B C D E") + -- local format = + -- "t=%02d: %s %s %s %s %s" + -- Main loop: + for t = 0, 79 do + if t < 20 then + f = bxor(d, band(b, bxor(c, d))) + k = 0x5A827999 + elseif t < 40 then + f = bxor(b, c, d) + k = 0x6ED9EBA1 + elseif t < 60 then + f = bor(band(b, c), (band(d, bor(b, c)))) + k = 0x8F1BBCDC + else + f = bxor(b, c, d) + k = 0xCA62C1D6 + end + e, d, c, b, a = + d, + c, + rol(b, 30), + a, + tobit( + unsigned(rol(a, 5)) + + unsigned(f) + + unsigned(e) + + unsigned(k) + + W[t] + ) + -- print(string.format(format, t, tohex(a), tohex(b), tohex(c), tohex(d), tohex(e))) + end + + -- Add this chunk's hash to result so far: + h0 = tobit(unsigned(h0) + a) + h1 = tobit(unsigned(h1) + b) + h2 = tobit(unsigned(h2) + c) + h3 = tobit(unsigned(h3) + d) + h4 = tobit(unsigned(h4) + e) + + -- The block is now reusable. + offset = 0 + for i = 0, 15 do + W[i] = 0 + end + end + + return { + update = update, + digest = digest + } + +end + +return function (buffer) + -- Pass in false or nil to get a streaming interface. + if not buffer then + return create(false) + end + local shasum = create(true) + shasum.update(buffer) + return shasum.digest() +end diff --git a/deps/sqlite3.lua b/deps/sqlite3.lua new file mode 100644 index 0000000..c49518f --- /dev/null +++ b/deps/sqlite3.lua @@ -0,0 +1,638 @@ +-------------------------------------------------------------------------------- +-- A library for interfacing with SQLite3 databases. +-- +-- Copyright (C) 2011-2016 Stefano Peluchetti. All rights reserved. +-- +-- Features, documentation and more: http://www.scilua.org . +-- +-- This file is part of the LJSQLite3 library, which is released under the MIT +-- license: full text in file LICENSE.TXT in the library's root folder. +-------------------------------------------------------------------------------- +--[[lit-meta + name = "SinisterRectus/sqlite3" + version = "1.0.1" + homepage = "http://scilua.org/ljsqlite3.html" + description = "SciLua's sqlite3 bindings repackaged for lit." + tags = {"database", "sqlite3"} + license = "MIT" + author = "Stefano Peluchetti" + contributors = { + "Sinister Rectus" + } +]] +-------------------------------------------------------------------------------- + +-- TODO: Refactor according to latest style / coding guidelines. +-- TODO: introduce functionality to get of a defined type to avoid if check? +-- TODO: Add extended error codes from Sqlite? +-- TODO: Consider type checks? +-- TODO: Exposed cdef constants are ok? +-- TODO: Resultset (and so exec) could be optimized by avoiding loads/stores +-- TODO: of row table via _step? + +local ffi = require "ffi" +local bit = require "bit" +local jit = require "jit" + +---- xsys replacement ---------------------------------------------------------- + +local insert = table.insert +local match, gmatch = string.match, string.gmatch + +local function split(str, delim) + local words = {} + for word in gmatch(str .. delim, '(.-)' .. delim) do + insert(words, word) + end + return words +end + +local function trim(str) + return match(str, '^%s*(.-)%s*$') +end + +-------------------------------------------------------------------------------- + +local function err(code, msg) + error("ljsqlite3["..code.."] "..msg) +end + +-- Codes ----------------------------------------------------------------------- +local sqlconstants = {} -- SQLITE_* and OPEN_* declarations. +local codes = { + [0] = "OK", "ERROR", "INTERNAL", "PERM", "ABORT", "BUSY", "LOCKED", "NOMEM", + "READONLY", "INTERRUPT", "IOERR", "CORRUPT", "NOTFOUND", "FULL", "CANTOPEN", + "PROTOCOL", "EMPTY", "SCHEMA", "TOOBIG", "CONSTRAINT", "MISMATCH", "MISUSE", + "NOLFS", "AUTH", "FORMAT", "RANGE", "NOTADB", [100] = "ROW", [101] = "DONE" +} -- From 0 to 26. + +do + local types = { "INTEGER", "FLOAT", "TEXT", "BLOB", "NULL" } -- From 1 to 5. + + local opens = { + READONLY = 0x00000001; + READWRITE = 0x00000002; + CREATE = 0x00000004; + DELETEONCLOSE = 0x00000008; + EXCLUSIVE = 0x00000010; + AUTOPROXY = 0x00000020; + URI = 0x00000040; + MAIN_DB = 0x00000100; + TEMP_DB = 0x00000200; + TRANSIENT_DB = 0x00000400; + MAIN_JOURNAL = 0x00000800; + TEMP_JOURNAL = 0x00001000; + SUBJOURNAL = 0x00002000; + MASTER_JOURNAL = 0x00004000; + NOMUTEX = 0x00008000; + FULLMUTEX = 0x00010000; + SHAREDCACHE = 0x00020000; + PRIVATECACHE = 0x00040000; + WAL = 0x00080000; + } + + local t = sqlconstants + local pre = "static const int32_t SQLITE_" + for i=0,26 do t[#t+1] = pre..codes[i].."="..i..";\n" end + for i=100,101 do t[#t+1] = pre..codes[i].."="..i..";\n" end + for i=1,5 do t[#t+1] = pre..types[i].."="..i..";\n" end + pre = pre.."OPEN_" + for k,v in pairs(opens) do t[#t+1] = pre..k.."="..bit.tobit(v)..";\n" end +end + +-- Cdef ------------------------------------------------------------------------ +-- SQLITE_*, OPEN_* +ffi.cdef(table.concat(sqlconstants)) + +-- sqlite3*, ljsqlite3_* +ffi.cdef[[ +// Typedefs. +typedef struct sqlite3 sqlite3; +typedef struct sqlite3_stmt sqlite3_stmt; +typedef void (*sqlite3_destructor_type)(void*); +typedef struct sqlite3_context sqlite3_context; +typedef struct Mem sqlite3_value; + +// Get informative error message. +const char *sqlite3_errmsg(sqlite3*); + +// Connection. +int sqlite3_open_v2(const char *filename, sqlite3 **ppDb, int flags, + const char *zVfs); +int sqlite3_close(sqlite3*); +int sqlite3_busy_timeout(sqlite3*, int ms); + +// Statement. +int sqlite3_prepare_v2(sqlite3 *conn, const char *zSql, int nByte, + sqlite3_stmt **ppStmt, const char **pzTail); +int sqlite3_step(sqlite3_stmt*); +int sqlite3_reset(sqlite3_stmt *pStmt); +int sqlite3_finalize(sqlite3_stmt *pStmt); + +// Extra functions for SELECT. +int sqlite3_column_count(sqlite3_stmt *pStmt); +const char *sqlite3_column_name(sqlite3_stmt*, int N); +int sqlite3_column_type(sqlite3_stmt*, int iCol); + +// Get value from SELECT. +int64_t sqlite3_column_int64(sqlite3_stmt*, int iCol); +double sqlite3_column_double(sqlite3_stmt*, int iCol); +int sqlite3_column_bytes(sqlite3_stmt*, int iCol); +const unsigned char *sqlite3_column_text(sqlite3_stmt*, int iCol); +const void *sqlite3_column_blob(sqlite3_stmt*, int iCol); + +// Set value in bind. +int sqlite3_bind_int64(sqlite3_stmt*, int, int64_t); +int sqlite3_bind_double(sqlite3_stmt*, int, double); +int sqlite3_bind_null(sqlite3_stmt*, int); +int sqlite3_bind_text(sqlite3_stmt*, int, const char*, int n, void(*)(void*)); +int sqlite3_bind_blob(sqlite3_stmt*, int, const void*, int n, void(*)(void*)); + +// Clear bindings. +int sqlite3_clear_bindings(sqlite3_stmt*); + +// Get value in callbacks. +int sqlite3_value_type(sqlite3_value*); +int64_t sqlite3_value_int64(sqlite3_value*); +double sqlite3_value_double(sqlite3_value*); +int sqlite3_value_bytes(sqlite3_value*); +const unsigned char *sqlite3_value_text(sqlite3_value*); //Not used. +const void *sqlite3_value_blob(sqlite3_value*); + +// Set value in callbacks. +void sqlite3_result_error(sqlite3_context*, const char*, int); +void sqlite3_result_int64(sqlite3_context*, int64_t); +void sqlite3_result_double(sqlite3_context*, double); +void sqlite3_result_null(sqlite3_context*); +void sqlite3_result_text(sqlite3_context*, const char*, int, void(*)(void*)); +void sqlite3_result_blob(sqlite3_context*, const void*, int, void(*)(void*)); + +// Persistency of data in callbacks (here just a pointer for tagging). +void *sqlite3_aggregate_context(sqlite3_context*, int nBytes); + +// Typedefs for callbacks. +typedef void (*ljsqlite3_cbstep)(sqlite3_context*,int,sqlite3_value**); +typedef void (*ljsqlite3_cbfinal)(sqlite3_context*); + +// Register callbacks. +int sqlite3_create_function( + sqlite3 *conn, + const char *zFunctionName, + int nArg, + int eTextRep, + void *pApp, + void (*xFunc)(sqlite3_context*,int,sqlite3_value**), + void (*xStep)(sqlite3_context*,int,sqlite3_value**), + void (*xFinal)(sqlite3_context*) +); +]] + +-------------------------------------------------------------------------------- +local sql = ffi.load("sqlite3") + +local transient = ffi.cast("sqlite3_destructor_type", -1) +local int64_ct = ffi.typeof("int64_t") + +local blob_mt = {} -- For tagging only. + +local function blob(str) + return setmetatable({ str }, blob_mt) +end + +local connstmt = {} -- Statements for a conn. +local conncb = {} -- Callbacks for a conn. +local aggregatestate = {} -- Aggregate states. + +local stmt_step + +local stmt_mt, stmt_ct = {} +stmt_mt.__index = stmt_mt + +local conn_mt, conn_ct = {} +conn_mt.__index = conn_mt + +-- Checks ---------------------------------------------------------------------- + +-- Helper function to get error msg and code from sqlite. +local function codemsg(pconn, code) + return codes[code]:lower(), ffi.string(sql.sqlite3_errmsg(pconn)) +end + +-- Throw error for a given connection. +local function E_conn(pconn, code) + local code, msg = codemsg(pconn, code) + err(code, msg) +end + +-- Test code is OK or throw error for a given connection. +local function T_okcode(pconn, code) + if code ~= sql.SQLITE_OK then + E_conn(pconn, code) + end +end + +local function T_open(x) + if x._closed then + err("misuse", "object is closed") + end +end + +-- Getters / Setters to minimize code duplication ------------------------------ +local sql_get_code = [=[ +return function(stmt_or_value ) + local t = sql.sqlite3__type(stmt_or_value ) + if t == sql.SQLITE_INTEGER then + return sql.sqlite3__int64(stmt_or_value ) + elseif t == sql.SQLITE_FLOAT then + return sql.sqlite3__double(stmt_or_value ) + elseif t == sql.SQLITE_TEXT then + local nb = sql.sqlite3__bytes(stmt_or_value ) + return ffi.string(sql.sqlite3__text(stmt_or_value ), nb) + elseif t == sql.SQLITE_BLOB then + local nb = sql.sqlite3__bytes(stmt_or_value ) + return ffi.string(sql.sqlite3__blob(stmt_or_value ), nb) + elseif t == sql.SQLITE_NULL then + return nil + else + err("constraint", "unexpected SQLite3 type") + end +end +]=] + +local sql_set_code = [=[ +return function(stmt_or_value, v ) + local t = type(v) + if ffi.istype(int64_ct, v) then + return sql.sqlite3__int64(stmt_or_value , v) + elseif t == "number" then + return sql.sqlite3__double(stmt_or_value , v) + elseif t == "string" then + return sql.sqlite3__text(stmt_or_value , v, #v, + transient) + elseif t == "table" and getmetatable(v) == blob_mt then + v = v[1] + return sql.sqlite3__blob(stmt_or_value , v, #v, + transient) + elseif t == "nil" then + return sql.sqlite3__null(stmt_or_value ) + else + err("constraint", "unexpected Lua type") + end +end +]=] + +-- Environment for setters/getters. +local sql_env = { + sql = sql, + transient = transient, + ffi = ffi, + int64_ct = int64_ct, + blob_mt = blob_mt, + getmetatable = getmetatable, + err = err, + type = type +} + +local function sql_format(s, variant, index) + return s:gsub("", variant):gsub("", index) +end + +local function loadcode(s, env) + local ret = assert(loadstring(s)) + if env then setfenv(ret, env) end + return ret() +end + +-- Must always be called from *:_* function due to error level 4. +local get_column = loadcode(sql_format(sql_get_code, "column", ",i"), sql_env) +local get_value = loadcode(sql_format(sql_get_code, "value" , " "), sql_env) +local set_column = loadcode(sql_format(sql_set_code, "bind" , ",i"), sql_env) +local set_value = loadcode(sql_format(sql_set_code, "result", " "), sql_env) + +-- Connection ------------------------------------------------------------------ +local open_modes = { + ro = sql.SQLITE_OPEN_READONLY, + rw = sql.SQLITE_OPEN_READWRITE, + rwc = bit.bor(sql.SQLITE_OPEN_READWRITE, sql.SQLITE_OPEN_CREATE) +} + +local function open(str, mode) + mode = mode or "rwc" + mode = open_modes[mode] + if not mode then + err("constraint", "argument #2 to open must be ro, rw, or rwc") + end + local aptr = ffi.new("sqlite3*[1]") + -- Usually aptr is set even if error code, so conn always needs to be closed. + local code = sql.sqlite3_open_v2(str, aptr, mode, nil) + local conn = conn_ct(aptr[0], false) + -- Must create this anyway due to conn:close() function. + connstmt[conn] = setmetatable({}, { __mode = "k" }) + conncb[conn] = { scalar = {}, step = {}, final = {} } + if code ~= sql.SQLITE_OK then + local code, msg = codemsg(conn._ptr, code) -- Before closing! + conn:close() -- Free resources, should not fail here in this case! + err(code, msg) + end + return conn +end + +function conn_mt:close() T_open(self) + -- Close all stmt linked to conn. + for k,_ in pairs(connstmt[self]) do if not k._closed then k:close() end end + -- Close all callbacks linked to conn. + for _,v in pairs(conncb[self].scalar) do v:free() end + for _,v in pairs(conncb[self].step) do v:free() end + for _,v in pairs(conncb[self].final) do v:free() end + local code = sql.sqlite3_close(self._ptr) + T_okcode(self._ptr, code) + connstmt[self] = nil -- Table connstmt is not weak, need to clear manually. + conncb[self] = nil + self._closed = true -- Set only if close succeded. +end + +function conn_mt:__gc() + if not self._closed then self:close() end +end + +function conn_mt:prepare(stmtstr) T_open(self) + local aptr = ffi.new("sqlite3_stmt*[1]") + -- If error code aptr NULL, so no need to close anything. + local code = sql.sqlite3_prepare_v2(self._ptr, stmtstr, #stmtstr, aptr, nil) + T_okcode(self._ptr, code) + local stmt = stmt_ct(aptr[0], false, self._ptr, code) + connstmt[self][stmt] = true + return stmt +end + +-- Connection exec, __call, rowexec -------------------------------------------- +function conn_mt:exec(commands, get) T_open(self) + local cmd1 = split(commands, ";") + local res, n + for i=1,#cmd1 do + local cmd = trim(cmd1[i]) + if #cmd > 0 then + local stmt = self:prepare(cmd) + res, n = stmt:resultset(get) + stmt:close() + end + end + return res, n -- Only last record is returned. +end + +function conn_mt:rowexec(command) T_open(self) + local stmt = self:prepare(command) + local res = stmt:_step() + if stmt:_step() then + err("misuse", "multiple records returned, 1 expected") + end + stmt:close() + if res then + return unpack(res) + else + return nil + end +end + +function conn_mt:__call(commands, out) T_open(self) + out = out or print + local cmd1 = split(commands, ";") + for c=1,#cmd1 do + local cmd = trim(cmd1[c]) + if #cmd > 0 then + local stmt = self:prepare(cmd) + local ret, n = stmt:resultset() + if ret then -- All the results get handled, not only last one. + out(unpack(ret[0])) -- Headers are printed. + for i=1,n do + local o = {} + for j=1,#ret[0] do + local v = ret[j][i] + if type(v) == "nil" then v = "" end -- Empty strings for NULLs. + o[#o+1] = tostring(v) + end + out(unpack(o)) + end + end + stmt:close() + end + end +end + +-- Callbacks ------------------------------------------------------------------- +-- Update (one of) callbacks registry for sqlite functions. +local function updatecb(self, where, name, f) + local cbs = conncb[self][where] + if cbs[name] then -- Callback already present, free old one. + cbs[name]:free() + end + cbs[name] = f -- Could be nil and that's fine. +end + +-- Return manually casted callback that sqlite expects, scalar. +local function scalarcb(name, f) + local values = {} -- Conversion buffer. + local function sqlf(context, nvalues, pvalues) + -- Indexing 0,N-1. + for i=1,nvalues do values[i] = get_value(pvalues[i - 1]) end + -- Throw error via sqlite function if necessary. + local ok, result = pcall(f, unpack(values, 1, nvalues)) + if not ok then + local msg = "Lua registered scalar function "..name.." error: "..result + sql.sqlite3_result_error(context, msg, #msg) + else + set_value(context, result) + end + end + return ffi.cast("ljsqlite3_cbstep", sqlf) +end + +-- Return the state for aggregate case (created via initstate()). We use the ptr +-- returned from aggregate_context() for tagging only, all the state data is +-- handled from Lua side. +local function getstate(context, initstate, size) + -- Only pointer address relevant for indexing, size irrelevant. + local ptr = sql.sqlite3_aggregate_context(context, size) + local pid = tonumber(ffi.cast("intptr_t",ptr)) + local state = aggregatestate[pid] + if type(state) == "nil" then + state = initstate() + aggregatestate[pid] = state + end + return state, pid +end + +-- Return manually casted callback that sqlite expects, stepper for aggregate. +local function stepcb(name, f, initstate) + local values = {} -- Conversion buffer. + local function sqlf(context, nvalues, pvalues) + -- Indexing 0,N-1. + for i=1,nvalues do values[i] = get_value(pvalues[i - 1]) end + local state = getstate(context, initstate, 1) + -- Throw error via sqlite function if necessary. + local ok, result = pcall(f, state, unpack(values, 1, nvalues)) + if not ok then + local msg = "Lua registered step function "..name.." error: "..result + sql.sqlite3_result_error(context, msg, #msg) + end + end + return ffi.cast("ljsqlite3_cbstep", sqlf) +end + +-- Return manually casted callback that sqlite expects, finalizer for aggregate. +local function finalcb(name, f, initstate) + local function sqlf(context) + local state, pid = getstate(context, initstate, 0) + aggregatestate[pid] = nil -- Clear the state. + local ok, result = pcall(f, state) + -- Throw error via sqlite function if necessary. + if not ok then + local msg = "Lua registered final function "..name.." error: "..result + sql.sqlite3_result_error(context, msg, #msg) + else + set_value(context, result) + end + end + return ffi.cast("ljsqlite3_cbfinal", sqlf) +end + +function conn_mt:setscalar(name, f) T_open(self) + jit.off(stmt_step) -- Necessary to avoid bad calloc in some use cases. + local cbf = f and scalarcb(name, f) or nil + local code = sql.sqlite3_create_function(self._ptr, name, -1, 5, nil, + cbf, nil, nil) -- If cbf nil this clears the function is sqlite. + T_okcode(self._ptr, code) + updatecb(self, "scalar", name, cbf) -- Update and clear old. +end + +function conn_mt:setaggregate(name, initstate, step, final) T_open(self) + jit.off(stmt_step) -- Necessary to avoid bad calloc in some use cases. + local cbs = step and stepcb (name, step, initstate) or nil + local cbf = final and finalcb(name, final, initstate) or nil + local code = sql.sqlite3_create_function(self._ptr, name, -1, 5, nil, + nil, cbs, cbf) -- If cbs, cbf nil this clears the function is sqlite. + T_okcode(self._ptr, code) + updatecb(self, "step", name, cbs) -- Update and clear old. + updatecb(self, "final", name, cbf) -- Update and clear old. +end + +conn_ct = ffi.metatype("struct { sqlite3* _ptr; bool _closed; }", conn_mt) + +-- Statement ------------------------------------------------------------------- +function stmt_mt:reset() T_open(self) + -- Ignore possible error code, it would be repetition of error raised during + -- most recent evaluation of statement which would have been raised already. + sql.sqlite3_reset(self._ptr) + self._code = sql.SQLITE_OK -- Always succeds. + return self +end + +function stmt_mt:close() T_open(self) + -- Ignore possible error code, it would be repetition of error raised during + -- most recent evaluation of statement which would have been raised already. + sql.sqlite3_finalize(self._ptr) + self._code = sql.SQLITE_OK -- Always succeds. + self._closed = true -- Must be called exaclty once. +end + +function stmt_mt:__gc() + if not self._closed then self:close() end +end + +-- Statement step, resultset --------------------------------------------------- +function stmt_mt:_ncol() + return sql.sqlite3_column_count(self._ptr) +end + +function stmt_mt:_header(h) + for i=1,self:_ncol() do -- Here indexing 0,N-1. + h[i] = ffi.string(sql.sqlite3_column_name(self._ptr, i - 1)) + end +end + +stmt_step = function(self, row, header) + -- Must check code ~= SQL_DONE or sqlite3_step --> undefined result. + if self._code == sql.SQLITE_DONE then return nil end -- Already finished. + self._code = sql.sqlite3_step(self._ptr) + if self._code == sql.SQLITE_ROW then + -- All the sql.* functions called never errors here. + row = row or {} + for i=1,self:_ncol() do + row[i] = get_column(self._ptr, i - 1) + end + if header then self:_header(header) end + return row, header + elseif self._code == sql.SQLITE_DONE then -- Have finished now. + return nil + else -- If code not DONE or ROW then it's error. + E_conn(self._conn, self._code) + end +end +stmt_mt._step = stmt_step + +function stmt_mt:step(row, header) T_open(self) + return self:_step(row, header) +end + +function stmt_mt:resultset(get, maxrecords) T_open(self) + get = get or "hik" + maxrecords = maxrecords or math.huge + if maxrecords < 1 then + err("constraint", "agument #1 to resultset must be >= 1") + end + local hash, hasi, hask = get:find("h"), get:find("i"), get:find("k") + local r, h = self:_step({}, {}) + if not r then return nil, 0 end -- No records case. + -- First record, o is a temporary table used to get records. + local o = hash and { [0] = h } or {} + for i=1,#h do o[i] = { r[i] } end + -- Other records. + local n = 1 + while n < maxrecords and self:_step(r) do + n = n + 1 + for i=1,#h do o[i][n] = r[i] end + end + + local out = { [0] = o[0] } -- Eventually copy colnames. + if hasi then -- Use numeric indexes. + for i=1,#h do out[i] = o[i] end + end + if hask then -- Use colnames indexes. + for i=1,#h do out[h[i]] = o[i] end + end + return out, n +end + +-- Statement bind -------------------------------------------------------------- +function stmt_mt:_bind1(i, v) + local code = set_column(self._ptr, v, i) -- Here indexing 1,N. + T_okcode(self._conn, code) + return self +end + +function stmt_mt:bind1(i, v) T_open(self) + return self:_bind1(i, v) +end + +function stmt_mt:bind(...) T_open(self) + for i=1,select("#", ...) do self:_bind1(i, select(i, ...)) end + return self +end + +function stmt_mt:clearbind() T_open(self) + local code = sql.sqlite3_clear_bindings(self._ptr) + T_okcode(self._conn, code) + return self +end + +stmt_ct = ffi.metatype([[struct { + sqlite3_stmt* _ptr; + bool _closed; + sqlite3* _conn; + int32_t _code; +}]], stmt_mt) + +return { + open = open, + blob = blob, +} diff --git a/deps/websocket-codec.lua b/deps/websocket-codec.lua new file mode 100644 index 0000000..3ea1611 --- /dev/null +++ b/deps/websocket-codec.lua @@ -0,0 +1,301 @@ +--[[lit-meta + name = "creationix/websocket-codec" + description = "A codec implementing websocket framing and helpers for handshakeing" + version = "3.0.2" + dependencies = { + "creationix/base64@2.0.0", + "creationix/sha1@1.0.0", + } + homepage = "https://github.com/luvit/lit/blob/master/deps/websocket-codec.lua" + tags = {"http", "websocket", "codec"} + license = "MIT" + author = { name = "Tim Caswell" } +]] + +local base64 = require('base64').encode +local sha1 = require('sha1') +local bit = require('bit') + +local band = bit.band +local bor = bit.bor +local bxor = bit.bxor +local rshift = bit.rshift +local lshift = bit.lshift +local char = string.char +local byte = string.byte +local sub = string.sub +local gmatch = string.gmatch +local lower = string.lower +local gsub = string.gsub +local concat = table.concat +local floor = math.floor +local random = math.random + +local function rand4() + -- Generate 32 bits of pseudo random data + local num = floor(random() * 0x100000000) + -- Return as a 4-byte string + return char( + rshift(num, 24), + band(rshift(num, 16), 0xff), + band(rshift(num, 8), 0xff), + band(num, 0xff) + ) +end + +local function applyMask(data, mask) + local bytes = { + [0] = byte(mask, 1), + [1] = byte(mask, 2), + [2] = byte(mask, 3), + [3] = byte(mask, 4) + } + local out = {} + for i = 1, #data do + out[i] = char( + bxor(byte(data, i), bytes[(i - 1) % 4]) + ) + end + return concat(out) +end + +local function decode(chunk, index) + local start = index - 1 + local length = #chunk - start + if length < 2 then return end + local second = byte(chunk, start + 2) + local len = band(second, 0x7f) + local offset + if len == 126 then + if length < 4 then return end + len = bor( + lshift(byte(chunk, start + 3), 8), + byte(chunk, start + 4)) + offset = 4 + elseif len == 127 then + if length < 10 then return end + len = bor( + lshift(byte(chunk, start + 3), 24), + lshift(byte(chunk, start + 4), 16), + lshift(byte(chunk, start + 5), 8), + byte(chunk, start + 6) + ) * 0x100000000 + bor( + lshift(byte(chunk, start + 7), 24), + lshift(byte(chunk, start + 8), 16), + lshift(byte(chunk, start + 9), 8), + byte(chunk, start + 10) + ) + offset = 10 + else + offset = 2 + end + local mask = band(second, 0x80) > 0 + if mask then + offset = offset + 4 + end + offset = offset + start + if #chunk < offset + len then return end + + local first = byte(chunk, start + 1) + local payload = sub(chunk, offset + 1, offset + len) + assert(#payload == len, "Length mismatch") + if mask then + payload = applyMask(payload, sub(chunk, offset - 3, offset)) + end + return { + fin = band(first, 0x80) > 0, + rsv1 = band(first, 0x40) > 0, + rsv2 = band(first, 0x20) > 0, + rsv3 = band(first, 0x10) > 0, + opcode = band(first, 0xf), + mask = mask, + len = len, + payload = payload + }, offset + len + 1 +end + +local function encode(item) + if type(item) == "string" then + item = { + opcode = 2, + payload = item + } + end + local payload = item.payload + assert(type(payload) == "string", "payload must be string") + local len = #payload + local fin = item.fin + if fin == nil then fin = true end + local rsv1 = item.rsv1 + local rsv2 = item.rsv2 + local rsv3 = item.rsv3 + local opcode = item.opcode or 2 + local mask = item.mask + local chars = { + char(bor( + fin and 0x80 or 0, + rsv1 and 0x40 or 0, + rsv2 and 0x20 or 0, + rsv3 and 0x10 or 0, + opcode + )), + char(bor( + mask and 0x80 or 0, + len < 126 and len or (len < 0x10000) and 126 or 127 + )) + } + if len >= 0x10000 then + local high = len / 0x100000000 + chars[3] = char(band(rshift(high, 24), 0xff)) + chars[4] = char(band(rshift(high, 16), 0xff)) + chars[5] = char(band(rshift(high, 8), 0xff)) + chars[6] = char(band(high, 0xff)) + chars[7] = char(band(rshift(len, 24), 0xff)) + chars[8] = char(band(rshift(len, 16), 0xff)) + chars[9] = char(band(rshift(len, 8), 0xff)) + chars[10] = char(band(len, 0xff)) + elseif len >= 126 then + chars[3] = char(band(rshift(len, 8), 0xff)) + chars[4] = char(band(len, 0xff)) + end + if mask then + local key = rand4() + return concat(chars) .. key .. applyMask(payload, key) + end + return concat(chars) .. payload +end + +local websocketGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + +-- Given two hex characters, return a single character +local function hexToBin(cc) + return string.char(tonumber(cc, 16)) +end + +local function decodeHex(hex) + local bin = string.gsub(hex, "..", hexToBin) + return bin +end + +local function acceptKey(key) + return gsub(base64(decodeHex(sha1(key .. websocketGuid))), "\n", "") +end + +-- Make a client handshake connection +local function handshake(options, request) + -- Generate 20 bytes of pseudo-random data + local key = concat({rand4(), rand4(), rand4(), rand4(), rand4()}) + key = base64(key) + local host = options.host + local path = options.path or "/" + local protocol = options.protocol + local req = { + method = "GET", + path = path, + {"Connection", "Upgrade"}, + {"Upgrade", "websocket"}, + {"Sec-WebSocket-Version", "13"}, + {"Sec-WebSocket-Key", key}, + } + for i = 1, #options do + req[#req + 1] = options[i] + end + if host then + req[#req + 1] = {"Host", host} + end + if protocol then + req[#req + 1] = {"Sec-WebSocket-Protocol", protocol} + end + local res = request(req) + if not res then + return nil, "Missing response from server" + end + -- Parse the headers for quick reading + if res.code ~= 101 then + return nil, "response must be code 101" + end + + local headers = {} + for i = 1, #res do + local name, value = unpack(res[i]) + headers[lower(name)] = value + end + + if not headers.connection or lower(headers.connection) ~= "upgrade" then + return nil, "Invalid or missing connection upgrade header in response" + end + if headers["sec-websocket-accept"] ~= acceptKey(key) then + return nil, "challenge key missing or mismatched" + end + if protocol and headers["sec-websocket-protocol"] ~= protocol then + return nil, "protocol missing or mistmatched" + end + return true +end + +local function handleHandshake(head, protocol) + + -- WebSocket connections must be GET requests + if not head.method == "GET" then return end + + -- Parse the headers for quick reading + local headers = {} + for i = 1, #head do + local name, value = unpack(head[i]) + headers[lower(name)] = value + end + + -- Must have 'Upgrade: websocket' and 'Connection: Upgrade' headers + if not (headers.connection and headers.upgrade and + headers.connection:lower():find("upgrade", 1, true) and + headers.upgrade:lower():find("websocket", 1, true)) then return end + + -- Make sure it's a new client speaking v13 of the protocol + if tonumber(headers["sec-websocket-version"]) < 13 then + return nil, "only websocket protocol v13 supported" + end + + local key = headers["sec-websocket-key"] + if not key then + return nil, "websocket security key missing" + end + + -- If the server wants a specified protocol, check for it. + if protocol then + local foundProtocol = false + local list = headers["sec-websocket-protocol"] + if list then + for item in gmatch(list, "[^, ]+") do + if item == protocol then + foundProtocol = true + break + end + end + end + if not foundProtocol then + return nil, "specified protocol missing in request" + end + end + + local accept = acceptKey(key) + + local res = { + code = 101, + {"Upgrade", "websocket"}, + {"Connection", "Upgrade"}, + {"Sec-WebSocket-Accept", accept}, + } + if protocol then + res[#res + 1] = {"Sec-WebSocket-Protocol", protocol} + end + + return res +end + +return { + decode = decode, + encode = encode, + acceptKey = acceptKey, + handshake = handshake, + handleHandshake = handleHandshake, +} diff --git a/libraries/.tasklib.lua.swp b/libraries/.tasklib.lua.swp new file mode 100644 index 0000000000000000000000000000000000000000..afb72ebcda7968d4470999237bec751c8c260359 GIT binary patch literal 12288 zcmeI2zi$&s6vqcB`3=(1p@*T^MTwms4Fn;SfDz#mK*l)rKLM5DiFgN*f#c)cDf8TV-PHT%KK3fzh?Ov&~vNGvoPmEsol` z%yGS%k@^~}YTTEfx-7?-02BBs0$Z1+rzR)tmi?7|w0qamSMBCxCcp%k025#WOn?b6 z0Vco%ery7!w?W*-5jKvFv41pgcxN7&JTU<#zyz286JP>NfC(@GCcp%k025#WKOg}a z3h{EY5X)Q8Jih-g|NsAEyAV&HyU;D@26PDecbgE8pb(PKEOY?c0{yd9h`*t~pjXgu z&_n1BbQ0PF{fFzkfnGzuL$9Fc&@<>E^Z>dK-G*|=_H`L*LWiNf&?e{&dVT@@3f+h9 zK|_cK6JP>NfC(@GCcp%k02BCL33U2tV4^G~`-4|q&DDF3BpZyJ9}IbFpi(1L8h$1$ z2;*ZpAM>O1w};9Kc6MvmidJ!_8n$Od8FmP|G7V#u&xA6HS6YD#y2|r~J>J?T=cS`` z(EOPuoomc5&NeR4&-Dvq;&`Jz+pJS_c5b0wAopCZ-CH)K zuB*Vic+vTz4yBnPbv@EXd&_;5uT)JtZly9&V-YsrYXDXoKDA!qu#&QZH=xr0Mz}=G1CMX=GM&N*1{? z{XC_PjJ0y5%K%}lUt4>WZ1R4QYkfHQfR3V(!P@CMSx(o+1{9^#lTofcBilZOnJA83 zKtl874YlzxJ@Cc&I6gFG>)J82e-eGgPB6NWTR7v)(!xS%4e6K)x*6dYY4PSNxPGAAc&`J#x)Lb*bbcGa-7wZ_XaEEjhELZdhno-q#(nUCwZ z_s~1vY*<6)S2UGID2-Y*f-Fh!j2AY#u#KFUBd|@RnHixhZa+{)NiWNdS1BCRcuoq| l_Q@N)D#!|AskK*W!g;EAe^wJ+Xk8&^D_w0%t=t-!{{uQ`q$L0V literal 0 HcmV?d00001 diff --git a/libraries/air.lua b/libraries/air.lua new file mode 100644 index 0000000..dceeb64 --- /dev/null +++ b/libraries/air.lua @@ -0,0 +1,187 @@ +--rewrite this lib (P.S: done) +--P.S: air stands for Advanced Input Recognition, although technically it's not all that advanced +air = {} +air.match_strings = function(string) + local strings = {} + string = string:gsub("\"(.-[^\\])\"",function(capt) + string_id = string_id + 1 + strings["%str"..string_id] = capt:gsub("\\\"","\"") + return " %str"..string_id + end) + return string,strings +end + +--this table will look up special types +special_case = { + ["voiceChannel"] = function(id,client,guild_id) + local guild = client:getGuild(guild_id) + local channel = guild:getChannel(id:match("(%d+)[^%d]*$")) + if tostring(channel):match("^GuildVoiceChannel: ") then + return true,channel + else + return false + end + end, + ["textChannel"] = function(id,client,guild_id) + local guild = client:getGuild(guild_id) + local channel = guild:getChannel(id:match("(%d+)[^%d]*$")) + if tostring(channel):match("^GuildTextChannel: ") then + return true,channel + else + return false + end + end, + ["messageLink"] = function(id,client,guild_id) + local guild = client:getGuild(guild_id) + local channelId,messageId = id:match("(%d+)/(%d+)[^%d]*$") + channel = guild:getChannel(channelId) + if tostring(channel):find("GuildTextChannel") then + message = channel:getMessage(messageId) + if message then + return true,message + end + end + return false + end, + ["role"] = function(id,client,guild_id) + local guild = client:getGuild(guild_id) + local role = guild:getRole(id:match("(%d+)[^%d]*$")) + if role then + return true,role + else + return false + end + end, + ["member"] = function(id,client,guild_id) + local guild = client:getGuild(guild_id) + local member = guild:getMember(id:match("(%d+)[^%d]*$")) + if member then + return true,member + else + return false + end + end, + ["emoji"] = function(id,client,guild_id) + local guild = client:getGuild(guild_id) + local emoji = guild:getEmoji(id:match("(%d+)[^%d]*$")) + if emoji then + return true,emoji + else + return false + end + end, + ["ban"] = function(id,client,guild_id) + local guild = client:getGuild(guild_id) + local ban = guild:getBan(id:match("(%d+)[^%d]*$")) + if ban then + return true,ban + else + return false + end + end, + ["channel"] = function(id,client,guild_id) + local guild = client:getGuild(guild_id) + local channel = guild:getChannel(id:match("(%d+)[^%d]*$")) + if channel then + return true,channel + else + return false + end + end, + ["user"] = function(id,client,guild_id) + local user = client:getUser(id:match("(%d+)[^%d]*$")) + if user then + return true,user + end + return false + end, + ["id"] = function(id) + if tonumber(id:match("(%d+)[^%d]*$")) and tostring(id:match("(%d+)[^%d]*$")):len() > 10 then + return true,id + end + return false + end +} + +air.parse = function(string,argmatch,client,guild_id) + local args,opts = {},{} + local string_id = 0 + local strings = {} + string = string:gsub("[%s\n]+\"\"",function(capt) + string_id = string_id + 1 + strings["%str"..string_id] = "" + return " %str"..string_id + end) + string = string:gsub("[%s\n]+\"(.-[^\\])\"",function(capt) + string_id = string_id + 1 + strings["%str"..string_id] = capt:gsub("\\\"","\"") + return " %str"..string_id + end) + string = string:gsub("[%s\n]+%-%-(%w+)=\"\"",function(name) + opts[name] = "" + return "" + end) + string = string:gsub("[%s\n]+%-%-(%w+)=\"(.-[^\\])\"",function(name,value) + opts[name] = value:gsub("\\\"","\"") + return "" + end) + string = string:gsub("[%s\n]+%-%-(%w+)=(%S+)",function(name,value) + opts[name] = value + return "" + end) + string = string:gsub("[%s\n]+%-%-(%w+)",function(name) + opts[name] = true + return "" + end) + string = string:gsub("[%s\n]+%-(%w+)",function(args) + args:gsub(".",function(key) + opts[key] = true + end) + return "" + end) + string:gsub("([^%s\n]+)",function(match) + table.insert(args,match) + end) + for k,v in pairs(args) do + if v:match("%%str%d+") then + if strings[v] then + args[k] = strings[v] + end + end + end + if argmatch and #argmatch > 0 then + local match,err = false + local new_args = {} + for k,v in pairs(argmatch) do + if not args[k] then + match = false + err = "Missing arguments: "..table.concat(argmatch,", ",k) + break + end + if v == "number" and tonumber(args[k]) then + match = true + new_args[k] = tonumber(args[k]) + elseif v == "string" then + match = true + new_args[k] = args[k] + elseif special_case[v] then + match,new_args[k] = special_case[v](args[k],client,guild_id) + else + match = false + end + if match == false then + err = "Type mismatch for argument "..k..": "..argmatch[k].." expected." + break + end + end + for k,v in pairs(args) do + if not new_args[k] then + new_args[k] = v + end + end + return match,new_args,opts,err + else + return true,args,opts + end +end +return air diff --git a/libraries/classes/.test/baseclass.lua b/libraries/classes/.test/baseclass.lua new file mode 120000 index 0000000..447b114 --- /dev/null +++ b/libraries/classes/.test/baseclass.lua @@ -0,0 +1 @@ +../baseclass.lua \ No newline at end of file diff --git a/libraries/classes/.test/test.lua b/libraries/classes/.test/test.lua new file mode 100644 index 0000000..b25553f --- /dev/null +++ b/libraries/classes/.test/test.lua @@ -0,0 +1,88 @@ +class = require("baseclass") +color = require("tty-colors") +tests = {} +tests[1] = function() + print("Basic class initialization test") + local newclass = class("TestObject") + function newclass:__init(value) + self.prop = value + end + function newclass:setProp(value) + self.prop = value + print("Property for object "..tostring(self).." set to "..tostring(value)) + end + function newclass:printProp() + print(self.prop) + end + local object_a = newclass(3) + object_a:printProp() + object_a:setProp(30) + object_a:printProp() +end + +tests[2] = function() + print("Class instance independence test") + local newclass = class("TestObject") + function newclass:__init(value) + self.prop = value + end + function newclass:setProp(value) + self.prop = value + print("Property for object "..tostring(self).." set to "..tostring(value)) + end + function newclass:printProp() + print(self.prop) + end + local object_a = newclass(3) + local object_b = newclass() + object_a:printProp() + object_b:printProp() + object_a:setProp(30) + object_b:setProp(20) + object_a:printProp() + object_b:printProp() +end + +tests[3] = function() + print("Extension test") + local newclass = class("Accumulator") + function newclass:ret() + return self.acc + end + function newclass:setA(a) + self.a = a + end + function newclass:setB(b) + self.b = b + end + local adder = newclass:extend("Adder") + function adder:add() + self.acc = self.a + self.b + end + local subber = newclass:extend("Subtracter") + function subber:sub() + self.acc = self.a - self.b + end + obj1 = adder() + obj1:setA(1) + obj1:setB(2) + obj1:add() + print(obj1:ret()) + obj2 = subber() + obj2:setA(1) + obj2:setB(2) + obj2:sub() + print(obj2:ret()) +end +--here run tests +print("Deteceted "..#tests.." tests. Starting now.") +OK = 0 +for k,v in pairs(tests) do + status,errcode = pcall(v) + stat_color = (status and "green") or "red" + print(color("TEST #"..k.." "..((status and "OK") or "ERROR")..(((not status) and errcode) or ""),stat_color)) + if status then + OK = OK + 1 + end +end +print(OK.."/"..#tests.." tests completed successfully") diff --git a/libraries/classes/.test/tty-colors.lua b/libraries/classes/.test/tty-colors.lua new file mode 120000 index 0000000..949a7ac --- /dev/null +++ b/libraries/classes/.test/tty-colors.lua @@ -0,0 +1 @@ +../../tty-colors.lua \ No newline at end of file diff --git a/libraries/classes/acl.lua b/libraries/classes/acl.lua new file mode 100644 index 0000000..4029287 --- /dev/null +++ b/libraries/classes/acl.lua @@ -0,0 +1,92 @@ +--Access control list class +--Note that it isn't directly used by anything, +--Instead it is extended to work with discord's permission system +--as command-acl +local class = import("classes.baseclass") +local table_utils = import("table-utils") +local acl = class("ACL") +function acl:__init() + self.user_rules = {} + self.group_rules = {} +end +function acl:set_user_rule(user_id,status) + assert( + (status == nil) or (status == 0) or (status == -1) or (status == 1), + "invalid status setting" + ) + self.user_rules[user_id] = status +end +function acl:set_group_rule(group_id,status) + assert( + (status == nil) or (status == 0) or (status == -1) or (status == 1), + "invalid status setting" + ) + self.group_rules[group_id] = status +end +function acl:check_user(user_id) + if self.user_rules[user_id] and self.user_rules[user_id] ~= 0 then + return true,(self.user_rules[user_id] == 1) + else + return false + end +end +function acl:check_group(groups) + local allow = false + local found = false + for k,v in pairs(groups) do + if self.group_rules[v] then + found = true + allow = self.group_rules[v] + end + end + return found,(allow and allow == 1) +end +function acl:export_all_lists() + local lists = { + users = "", + groups = "" + } + for k,v in pairs(self.user_rules) do + lists.users = lists.users..k..":"..tostring(v)..";\n" + end + for k,v in pairs(self.group_rules) do + lists.groups = lists.groups..k..":"..tostring(v)..";\n" + end + return lists +end +function acl:export_user_list() + local list = "" + for k,v in pairs(self.user_rules) do + list = list..k..":"..tostring(v)..";\n" + end + return list +end +function acl:export_group_list() + local list = "" + for k,v in pairs(self.group_rules) do + list = list..k..":"..tostring(v)..";\n" + end + return list +end +function acl:export_snapshot() + return { + user_rules = bot_utils.deepcopy(self.user_rules), + group_rules = bot_utils.deepcopy(self.group_rules) + } +end +function acl:import_snapshot(t) + self.user_rules = t.user_rules + self.group_rules = t.group_rules +end +function acl:import_user_list(list) + list:gsub("(%w+):(%d+)",function(id,status) + self.user_rules[id] = status + end) +end +function acl:import_group_list(list) + list:gsub("(%w+):(%d+)",function(id,status) + self.group_rules[id] = status + end) +end + +return acl diff --git a/libraries/classes/baseclass.lua b/libraries/classes/baseclass.lua new file mode 100644 index 0000000..fbef02c --- /dev/null +++ b/libraries/classes/baseclass.lua @@ -0,0 +1,32 @@ +--class generator (for the purpose of creating classes) +return function(name) + local new_class = {} + new_class.__classname = name or "Object" + new_class.__index = new_class + new_class.__new = function(self,...) + local obj = {} + --set metamethod proetection measures + setmetatable(obj,{__index = function(obj,key) + if key:find("^__") then + return nil + else + return self[key] + end + end, + __name = new_class.__classname}) + if self.__init then + self.__init(obj,...) + end + return obj + end + new_class.extend = function(self,name) + local new_class = {} + new_class.__classname = name or "Object" + new_class.__index = new_class + setmetatable(new_class,{__index = self,__call = function(...) return new_class.__new(...) end, __name = new_class.__classname.." (class)"}) + return new_class + end + --make our class callable; on call, it will initialize a new instance of itself + setmetatable(new_class,{__call = function(...) return new_class.__new(...) end, __name = new_class.__classname.." (class)"}) + return new_class +end diff --git a/libraries/classes/command-handler.lua b/libraries/classes/command-handler.lua new file mode 100644 index 0000000..3cf09ac --- /dev/null +++ b/libraries/classes/command-handler.lua @@ -0,0 +1,107 @@ +--This class acts as a pipe between the incoming messages and commands. +--It observes the content of the incoming messages, and, depending on the optional flags, +--executes specific commands +--Remember: there can only be one command handler and plugin handler +--per a server handler. Side effects of using multiple command handlers and +--plugin handlers are unknown. +local class = import("classes.baseclass") +local command_handler = class("Command-handler") +local table_utils = import("table-utils") +local purify = import("purify") +function command_handler:__init(parent_server) + self.server_handler = assert(parent_server,"parent server handler not provided") + self.command_pool = {} + self.prefixes = {} + self.command_meta = { + plugins = {}, + } +end +function command_handler:add_prefix(prefix) + local purified_prefix = purify.purify_escapes(prefix) + self.prefixes[purified_prefix] = purified_prefix + return true +end +function command_handler:remove_prefix(prefix) + local purified_prefix = purify.purify_escapes(prefix) + if self.prefixes[purified_prefix] or table_utils.count(self.prefixes) <= 1 then + self.prefix[purified_prefix] = nil + return true + else + return false, ( + (self.prefixes[purified_prefix] and "No such prefix") or + "Cannot remove the last remaining prefix" + ) + end +end +function command_handler:get_prefixes() + return table_utils.deepcopy(self.prefixes) +end +function command_handler:add_command(command) + assert(type(command) == "table","command object expected") + local purified_name = purify.purify_escapes(command.name) + self.command_pool[purified_name] = command + if not self.command_meta.plugins[command.parent.name] then + self.command_meta.plugins[command.parent.name] = {} + end + table.insert(self.command_meta.plugins[command.parent.name],command.name) + return command +end +function command_handler:remove_command(command) + assert(type(command) == "table","command object expected") + local purified_name = purify.purify_escapes(command.name) + if self.command_pool[purified_name] then + local command = self.command_pool[purified_name] + --not exactly optimal, but lists are lists. can't do much about them. + table_utils.remove_value(self.command_meta.plugins[command.parent.name],command.name) + if #self.command_meta.plugins[command.parent.name] == 0 then + self.command_meta.plugins[command.parent.name] = nil + end + self.command_pool[purified_name] = nil + return true + else + return false + end +end +function command_handler:get_command(name) + local purified_name = purify.purify_escapes(assert(type(name) == "string") and name) + if self.command_pool[purified_name] then + return self.command_pool[purified_name] + else + return false + end +end +function command_handler:get_commands(name) + local list = {} + for k,v in pairs(self.command_pool) do + table.insert(list,k) + end + return list +end +function command_handler:get_commands_metadata() + return table_utils.deepcopy(self.command_meta) +end +function command_handler:handle(message) + for name,command in pairs(self.command_pool) do + if command.options.regex then + if message.content:match(command.options.regex) then + command:exec(message) + return + end + else + if command.options.prefix then + for _,prefix in pairs(self.prefixes) do + if message.content:find(prefix..name.."$") == 1 or message.content:find(prefix..name.."%s") then + command:exec(message) + return + end + end + else + if message.content:find(name.."$") == 1 or message.content:find(name.."%s") then + command:exec(message) + return + end + end + end + end +end +return command_handler diff --git a/libraries/classes/command.lua b/libraries/classes/command.lua new file mode 100644 index 0000000..c0f2012 --- /dev/null +++ b/libraries/classes/command.lua @@ -0,0 +1,163 @@ +--[[ +This class handles command management. +]] +local table_utils = import("table-utils") +local class = import("classes.baseclass") +local command = class("Command") +local acl = import("classes.command-acl") +function command:__init(name,callback) + self.rules = acl() + self.name = name + self.timer = os.time() + self.options = { + allow_bots = false, --allow bots to execute the command + typing_decorator = false, --set if the bot should be "typing" while the command executes + prefix = true, --if true and if regex isn't enabled, check for prefix at the start. if not, don't check for prefix + regex = false, --check if the message matches this regular expression (should be a string) + no_parsing = false, --check if you want to disable the message argument parsing process + timeout = 1000, --set the timeout for a command + } + if type(callback) == "table" then + for k,v in pairs(callback.options or {}) do + self.options[k] = v + end + self.callback = callback.exec + self.args = callback.args or self.args + if callback.users then + for k,v in pairs(callback.users) do + self.rules:set_user_rule(k,v) + end + end + if callback.roles then + for k,v in pairs(callback.roles) do + self.rules:set_group_rule(k,v) + end + end + if callback.perm then + self.rules:set_perm_rules(callback.perm) + end + if callback.help then + self:set_help(callback.help,callback.usage) + end + elseif type(callback) == "function" then + self.callback = callback + end +end +--set the callback to be called on comm:exec(msg) +function command:set_callback(fn) + assert(type(fn) == "function","function expected, got "..type(fn)) + self.callback = fn + return self +end +--generate help using only description and usage, or nothing at all +function command:generate_help(description,usage) + assert(not description or (type(description) == "string"),"Description should be either string or nil, got "..type(description)) + assert(not usage or (type(usage) == "string"),"Usage should be either string or nil, got "..type(usage)) + local backup_usage_str + if self.args then + backup_usage_str = self.name.." <"..table.concat(self.args,"> <")..">" + else + backup_usage_str = "not defined" + end + local permissions = table.concat(self.rules:export_snapshot()["perms"] or {},"\n") + if permissions == "" then + permissions = "All" + end + self.help = {embed = { + title = "Help for ``"..self.name.."``", + description = description, + fields = { + {name = "Usage: ",value = usage or backup_usage_str}, + {name = "Perms: ",value = permissions} + } + }} + return self +end +--set the help message to be sent +function command:set_help(obj,usage) + if type(obj) == "string" then + self:generate_help(obj,usage) + elseif type(obj) == "table" then + self.help = obj + else + error("Type "..type(obj).." cannot be set as a help message") + end + return self +end +--print the help message, or generate it if there is none +function command:get_help() + if not self.help then + self:generate_help("Description not defined") + end + return self.help +end +function command:set_timeout_callback(fn) + assert(type(fn) == "function","function expected, got "..type(fn)) + self.timeout_callback = fn + return self +end + +--check the permissions for command +function command:check_permissions(message) + if message.author.bot and (not self.options.allow_bots) then + return false + end + if discordia.Date():toMilliseconds()-self.options.timeout < self.timer then + if self.timeout_callback then + self.timeout_callback(fn) + return false + end + end + self.timer = discordia.Date():toMilliseconds() + if self.rules:check_user(message.author.id) then + local found,allow = self.rules:check_user(message.author.id) + return allow + end + if self.rules:check_group(message.member.roles) then + local found,allow = self.rules:check_group(message.member.roles) + return allow + end + return self.rules:check_perm(message.member:getPermissions(message.channel)) +end +--the main entry point for the command - execute the callback within after +--multiple checks +function command:exec(message,args,opts) + local exec = self.callback + if not self.callback then + error("Callback not set for command "..self.name) + end + if self.decorator then + self.callback = self.decorator(self.callback) + end + local content + if self.options.regex then + content = message.content + else + local strstart,strend = message.content:find(self.name,1,true) + content = message.content:sub(strend+1,-1) + end + if self:check_permissions(message) then + local status,args,opts,err = import("air").parse(content,self.args,message.client,message.guild.id) + if status then + self.callback(message,args,opts) + else + msg:reply(err) + end + end +end +--add decorators for the callback +function command:set_decorator(fn) + assert(type(fn) == "function","a decorator function expected, got "..type(fn)) + self.decorator = fn + return self +end +--get a list of all properties of the command +function command:get_properties() + return { + name = self.name, + args = table_utils.deepcopy(self.args), + help = table_utils.deepcopy(self.help), + prefix = self.prefix + } +end +return command diff --git a/libraries/classes/emitter-proxy.lua b/libraries/classes/emitter-proxy.lua new file mode 100644 index 0000000..d002851 --- /dev/null +++ b/libraries/classes/emitter-proxy.lua @@ -0,0 +1,95 @@ +local class = import("classes.baseclass") +local emitter_proxy = class("EmitterProxy") + +function emitter_proxy:__init(emitter) + self.original = emitter + self.callback_pool = {} +end + +function emitter_proxy:on(event,callback) + if not self.callback_pool[event] then + self.callback_pool[event] = {} + end + self.callback_pool[event][callback] = callback + self.original:on(event,callback) + return callback +end + +function emitter_proxy:once(event,callback) + if not self.callback_pool[event] then + self.callback_pool[event] = {} + end + local wrapper = function(...) + callback(...) + self.callback_pool[event][callback] = nil + end + self.callback_pool[event][callback] = wrapper + self.callback_pool[event][wrapper] = wrapper + self.original:once(event,wrapper) + return callback +end + +function emitter_proxy:removeListener(event,callback) + if self.callback_pool[event] and self.callback_pool[event][callback] then + self.callback_pool[event][callback] = nil + self.original:removeListener(event,callback) + end +end + +function emitter_proxy:removeAllListeners(event,callback) + if self.callback_pool[event] then + for k,v in pairs(self.callback_pool[event]) do + self.original:removeListener(event,v) + end + self.callback_pool[event] = nil + end +end + +function emitter_proxy:listeners(event) + local copy = {} + if self.callback_pool[event] then + for k,v in pairs(self.callback_pool[event]) do + table.insert(copy,v) + end + end + return copy +end + +function emitter_proxy:listenerCount(event) + local count = 0 + if event then + if self.callback_pool[event] then + for k,v in pairs(self.callback_pool[event]) do + count = count + 1 + end + end + else + for k,v in pairs(self.callback_pool) do + for k2,v2 in pairs(v) do + count = count + 1 + end + end + end + return count +end + +function emitter_proxy:propogate(event,emitter) + if not self.callback_pool[event] then + self.callback_pool[event] = {} + end + local emitter_propogate_handler = function(...) + emitter:emit(event,...) + end + self.callback_pool[event][emitter_propogate_handler] = emitter_propogate_handler + self.original:on(event,emitter_propogate_handler) + return emitter_propogate_handler +end + +function emitter_proxy:destroy() + for k,v in pairs(self.callback_pool) do + for k2,v2 in pairs(v) do + self.original:removeListener(k,v2) + end + end +end +return emitter_proxy diff --git a/libraries/classes/interfaces/discord.lua b/libraries/classes/interfaces/discord.lua new file mode 100644 index 0000000..fa0c455 --- /dev/null +++ b/libraries/classes/interfaces/discord.lua @@ -0,0 +1,12 @@ +local interface = {} +interface.wrapper = function(client,guild_id) + local new_i = {} + new_i.message = {} + new_i.message.get = function(channel,id) + local new_m = {} + local message = client.getMessage(id) + local new_m.content = message.content + local new_m.created_at = message.createdAt + local new_m.attachments = {} + for k,v in pairs(message.attachments) do + table.insert(new_m diff --git a/libraries/classes/monitor.lua b/libraries/classes/monitor.lua new file mode 100644 index 0000000..9548f63 --- /dev/null +++ b/libraries/classes/monitor.lua @@ -0,0 +1,90 @@ +local RPC_server = import("classes.RPC-server") +local class = import("classes.baseclass") +local monitor = class("Monitor") + +--we only generate proxies as far as 1 object deep. +--to provide seamlessness, metamethods that request object proxies from their +--pointers may be used on the client side + +--pointers here mean tables that contain the __id and __type properties. +--they do not hold any info on the object besides its class name and id + +--a lookup table of all classes that we do not ignore. we exclude client and containers +--because they might break the sandboxing. we *do not* want that. +local allowed_types = { + ["guild"] = true, + ["member"] = true, + ["emoji"] = true, + ["message"] = true, + ["channel"] = true, + ["role"] = true, + ["user"] = true, + ["invite"] = true, + ["guildtextchannel"] = true, + ["textchannel"] = true, + ["iterable"] = true, + ["cache"] = true, + ["arrayiterable"] = true, + ["filteretediterable"] = true, + ["secondarycache"] = true, + ["weakcache"] = true, + ["tableiterable"] = true, +} + +--a lookup table of classes that can be explicitly converted to arrays. +local iterable_types = { + ["iterable"] = true, + ["cache"] = true, + ["arrayiterable"] = true, + ["filteretediterable"] = true, + ["secondarycache"] = true, + ["weakcache"] = true, + ["tableiterable"] = true, +} + +local comprehend_object = function(object) + local output + if (type(object) == "table") and (object.__class) then + --our object is an instance of a class + local class = object.__class.__name:lower() + if allowed_types[class] and (not iterable_types[class]) then + --our object can only be pointed to + output = {__id = object[k].id, __type = class} + else + --our object can be converted to an array + + end + else + --our object is either an atomic data type, a string or a table. + + end +end + +local create_proxy = function(object) + local output = {} + for k,v in pairs(getmetatable(object).__getters) do + end +end + +local proto_api = { + msg = { + get = function(channel,id) + channel:getMessage(id) + end, + }, + guild = { + + }, + member = { + + }, + channel = { + + } +} + +function monitor:__init(guild,options) + assert(guild,"No guild provided") + assert(options,"No options provided (arg 2)") + + diff --git a/libraries/classes/plugin-handler.lua b/libraries/classes/plugin-handler.lua new file mode 100644 index 0000000..92ae61a --- /dev/null +++ b/libraries/classes/plugin-handler.lua @@ -0,0 +1,124 @@ +--This class manages the loading, unloading, scanning and event manipulation for plugin objects +--This class requries communication between itself and the command handler +--in order to load commands. +--Remember: there can only be one command handler and plugin handler +--per a server handler. Side effects of using multiple command handlers and +--plugin handlers are unknown. +local class = import("classes.baseclass") +local plugin_handler = class("PluginHandler") +local file = import("file") +local json = import("json") +local core = import("core") +local emitter_proxy = import("classes.emitter-proxy") +local table_utils = import("table-utils") +function plugin_handler:__init(parent_server) + assert(parent_server,"server handler to assign the plugin handler to has not been provided") + self.server_handler = parent_server + self.plugins = {} + self.plugin_info = {} + self.plugin_paths = {} +end + +function plugin_handler:add_plugin_folder(path) + assert(type(path) == "string","path should be a string, got "..type(path)) + table.insert(self.plugin_paths,path) +end + +function plugin_handler:scan_folder(path) + local file = io.open(path.."/meta.json","r") + if file then + local metadata,code,err = json.decode(file:read("*a")) + if metadata and metadata.name then + self.plugin_info[metadata.name] = metadata + self.plugin_info[metadata.name].path = path.."/" + self.plugin_info[metadata.name].loaded = false + end + file:close() + else + for k,v in pairs({"/init.lua","/main.lua"}) do + local file = io.open(path..v,"r") + if file then + local name = path:match("[^/]+$") + self.plugin_info[name] = {["main"]=v:gsub("/","")} + self.plugin_info[name].path = path.."/" + self.plugin_info[name].loaded = false + file:close() + end + end + end +end + +function plugin_handler:update_plugin_info() + for k,v in pairs(self.plugin_paths) do + if file.existsDir(v) then + file.ls(v):gsub("[^\n]+",function(c) + self:scan_folder(v..c) + end) + end + end +end + +function plugin_handler:list_loadable() + return table_utils.deepcopy(self.plugin_info) +end + +function plugin_handler:load(name) + if not self.plugin_info[name] then + return false, "No such plugin" + end + if not self.plugin_info[name].main then + return false, "Plugin metadata entry doesn't specify the main file path or main file isn't found" + end + if self.plugin_info[name].loaded then + return false, "Plugin is already loaded" + end + local environment = setmetatable({ + id = self.server_handler.id, + globals = self.server_handler.config, + signals = emitter_proxy(self.server_handler.signal_emitter), + client = self.server_handler.client, + events = emitter_proxy(self.server_handler.event_emitter), + discordia = import("discordia"), + server = self.server_handler, + command_handler = self.server_handler.command_handler, + plugin_handler = self.server_handler.plugin_handler, + log = function() end, + import = import, + },{__index = _G}) + local plugin_meta = self.plugin_info[name] + if file.exists(plugin_meta.path..plugin_meta.main) then + environment["plugin_path"] = plugin_meta.path + local plugin_content = file.read(plugin_meta.path..plugin_meta.main,"*a") + local plugin_loader,err = load(plugin_content,"plugin loader: "..plugin_meta.path..plugin_meta.main,nil,environment) + if plugin_loader then + local plugin_object = plugin_loader() + if plugin_object then + plugin_object.name = name + plugin_object:load(environment) + self.plugins[name] = plugin_object + self.plugins[name].__env = environment + self.plugin_info[name].loaded = true + return true + else + return false, "Plugin object missing" + end + else + return false, err + end + else + return false, "File specified as the main file is inaccessible" + end +end + +function plugin_handler:unload(name) + if self.plugins[name] then + self.plugins[name].__env.signals:destroy() + self.plugins[name].__env.events:destroy() + self.plugins[name]:unload() + self.plugin_info[name].loaded = false + return true + else + return false,"Plugin is not loaded" + end +end +return plugin_handler diff --git a/libraries/classes/plugin.lua b/libraries/classes/plugin.lua new file mode 100644 index 0000000..6ed4a92 --- /dev/null +++ b/libraries/classes/plugin.lua @@ -0,0 +1,58 @@ +local class = import("classes.baseclass") +local plugin = class("Plugin") + +function plugin:__init() + self.command_pool = {} +end + +function plugin:load(environment) + self.command_handler = environment.server.command_handler + for k,v in pairs(self.command_pool) do + self.command_handler:add_command(v) + end +end + +function plugin:unload() + if self.removal_callback then + self.removal_callback() + end + for k,v in pairs(self.command_pool) do + self.command_handler:remove_command(v) + end +end + +function plugin:for_all_commands(fn) + assert(type(fn)=="function","function expected, got "..type(fn)) + for k,v in pairs(self.command_pool) do + fn(v) + end +end + +function plugin:for_every_new_command(fn) + assert(type(fn)=="function","function expected, got "..type(fn)) + self.decorator = fn +end + +function plugin:add_command(command_object) + if self.decorator then + self.fn(command_object) + end + command_object.parent = self + self.command_pool[command_object] = command_object + --in post init state: we request the command handler to add the commands + --that way, we can link our plugin back to the command handler + if self.command_handler then + self.command_handler:add_command(command_object) + end +end + +function plugin:remove_command(command_object) + if self.command_pool[command_object] then + self.command_pool[command_object] = nil + end + --remove command after post-init state + if self.command_handler then + self.command_handler:remove_command(command_object) + end +end +return plugin diff --git a/libraries/classes/server-handler.lua b/libraries/classes/server-handler.lua new file mode 100644 index 0000000..e6327ee --- /dev/null +++ b/libraries/classes/server-handler.lua @@ -0,0 +1,91 @@ +local class = import("classes.baseclass") +local server_handler = class("ServerHandler") +local core = import("core") +local plugin_handler = import("classes.plugin-handler") +local command_handler = import("classes.command-handler") +local file = import("file") +local eventlist = import("eventlist") +local discordia = import("discordia") + +local function check_partitioning(id,...) + args = {...} + v = args[1] + if type(v) == "table" and v.guild and v.guild.id == id then + return true + elseif not (type(v) == "table") then + return true + elseif type(v) == "table" and (not v.guild) and (tostring(v):find("Guild: ")) and v.id == id then + return true + elseif type(v) == "table" and (not v.guild) and (v.message) and (v.message.guild.id == id) then + return true + else + return false + end +end + + +function server_handler:__init(client,guild,options) + assert(type(client) == "table","discordia client expected, got "..type(client)) + self.client = client + self.uptime = discordia.Date() + self.event_emitter = core.Emitter:new() + self.signal_emitter = core.Emitter:new() + self.plugin_handler = plugin_handler(self) + self.command_handler = command_handler(self) + self.id = guild.id + self.config = {} + --conifgurable properties + self.config_path = options.path or "./servers/%id/" + self.autosave = options.path or true + self.autosave_frequency = options.autosave_frequency or 10 + self.plugin_search_paths = options.plugin_search_paths or {"./plugins/"} + self.default_plugins = options.default_plugins or {"test"} + self.default_prefixes = options.default_prefixes or {"<@!"..self.client.user.id..">","&"} + + self.config_path = self.config_path:gsub("%%id",self.id) + self:load_config() + if not file.existsDir(self.config_path) then + os.execute("mkdir -p "..self.config_path) + end + for k,v in pairs(eventlist) do + self.client:on(v,function(...) + --check if the event is for this server, and then emit. + if check_partitioning(self.id,...) then + self.event_emitter:emit(v,...) + end + end) + end + self.client:on("messageCreate",function(msg) + if msg.guild.id == self.id then + self.command_handler:handle(msg) + end + end) + for _,path in pairs(self.plugin_search_paths) do + self.plugin_handler:add_plugin_folder(path) + end + self.plugin_handler:update_plugin_info() + for _,plugin_name in pairs(self.default_plugins) do + print(self.plugin_handler:load(plugin_name)) + end + for _,prefix in pairs(self.default_prefixes) do + self.command_handler:add_prefix(prefix) + end +end + +function server_handler:load_config(path) + if path then + self.config = file.readJSON(path,{}) + else + self.config = file.readJSON(self.config_path.."config.json") + end +end + +function server_handler:save_config(path) + if path then + file.writeJSON(path,self.config) + else + file.writeJSON(self.config_path.."config.json",self.config) + end + self.event_emitter("serverSaveConfig") +end +return server_handler diff --git a/libraries/esolangs/befunge.lua b/libraries/esolangs/befunge.lua new file mode 100644 index 0000000..1cf06f9 --- /dev/null +++ b/libraries/esolangs/befunge.lua @@ -0,0 +1,436 @@ +local seq = function(start,endt) + local new_table = {} + for I = start,endt do + table.insert(new_table,I) + end + return new_table +end + +math.randomseed(os.clock()+os.time()) + +local function longest(tab) + local a = 0 + for k,v in pairs(tab) do + if type(v) == "table" and #v > a then + a = v + end + end + return a +end + +local function to_befunge_unit(char) + local unit + if (char > 0) and (char < 256) then + unit = string.char(char) + else + unit = "%"..char + end + return unit +end + +local function from_befunge_unit(char) + if char:match("%%(%d+)") then + return tonumber(char:match("%%(%d+)")) + else + return string.byte(char) + end +end + +local field = function(source) + local new_field = {} + local lines = {} + source:gsub("[^\n]+",function(capt) table.insert(lines,capt) end) + new_field.max_field_width = 150 + new_field.max_field_height = 100 + local field_width = 0 + local field_height = 0 + for k,v in pairs(lines) do + new_field[k] = {} + v:gsub(".",function(capt) + if #new_field[k] < new_field.max_field_width then + table.insert(new_field[k],capt) + end + end) + if #new_field[k] > field_width then + field_width = #new_field[k] + end + end + field_height = #new_field + function new_field:expand(height,width) + if (height > new_field.max_field_height) or (width > new_field.max_field_width) then + return nil + end + for I = 1,height do + local row = self[I] + if not row then + table.insert(self,{}) + row = self[I] + end + while #row > width do + table.remove(row,#row) + end + while #row < width do + table.insert(row," ") + end + end + return true + end + new_field:expand(field_height,field_width) + new_field.height = field_height + new_field.width = field_width + --return the field itself, the width of the field and its height + return new_field +end + +local rw_head = function(field,commands) + local RW_head = {} + RW_head.read_mode = false + RW_head.coords = { + x = 1, + y = 1 + } + RW_head.stack = {} + function RW_head:push(value) + table.insert(self.stack,value) + + end + function RW_head:pop(value) + local value = self.stack[#self.stack] + table.remove(self.stack,#self.stack) + return value + end + function RW_head:walk() + self.coords = { + x = self.coords.x+self.direction.x, + y = self.coords.y+self.direction.y + } + if self.coords.y > field.height then + self.coords.y = 1 + elseif self.coords.y < 1 then + self.coords.y = field.height + end + if self.coords.x > field.width then + self.coords.x = 1 + elseif self.coords.x < 1 then + self.coords.x = field.width + end + end + function RW_head:execute() + if not self.read_mode then + if commands[field[self.coords.y][self.coords.x]] then + commands[field[self.coords.y][self.coords.x]](field,self) + end + else + if field[self.coords.y][self.coords.x] ~= "\"" then + self:push(string.byte(field[self.coords.y][self.coords.x])) + else + self.read_mode = false + end + end + end + RW_head.direction = { + x = 1, + y = 0 + } + return RW_head +end +interpreter_state = true +local befunge = {} +function befunge:init(source,args) + local required = { + handle_int_input = true, + handle_output = true, + handle_input = true, + } + args.handle_warning = args.handle_warning or function() end + args.handle_error = args.handle_error or function() end + for k,v in pairs(required) do + if not args[k] then + error("Unable to initialize module: "..k.." not specified") + end + end + befunge.interpreter_state = true + local opcount = 0 + local new_field = field(source) + local warn_args = function(field,head) + local instruction = field[head.coords.y][head.coords.x] + local pos = head.coords.x..":"..head.coords.y + args.handle_warning("One or more arguments not present on "..instruction.." at "..pos) + end + local commands = { + ["@"] = function() + befunge.interpreter_state = false + end, + ["v"] = function(field,head) + head.direction = { + x = 0, + y = 1 + } + end, + [">"] = function(field,head) + head.direction = { + x = 1, + y = 0 + } + end, + ["<"] = function(field,head) + head.direction = { + x = -1, + y = 0 + } + end, + ["^"] = function(field,head) + head.direction = { + x = 0, + y = -1 + } + end, + ["+"] = function(field,head) + local a,b = head:pop(),head:pop() + if not (a or b) then + warn_args(field,head) + return + end + head:push(a+b) + end, + ["-"] = function(field,head) + local a,b = head:pop(),head:pop() + if not (a or b) then + warn_args(field,head) + return + end + head:push(b-a) + end, + ["*"] = function(field,head) + local a,b = head:pop(),head:pop() + if not (a or b) then + warn_args(field,head) + return + end + head:push(a*b) + end, + ["/"] = function(field,head) + local a,b = head:pop(),head:pop() + if not (a or b) then + warn_args(field,head) + return + end + if a == 0 then + (args.handle_division_by_zero or function() + end)() + return + end + local result = b/a + head:push(math.floor(result)) + end, + ["%"] = function(field,head) + local a,b = head:pop(),head:pop() + if not (a or b) then + warn_args(field,head) + return + end + if a == 0 then + (args.handle_division_by_zero or function() + end)() + return + end + local result = math.fmod(b,a) + head:push(result) + end, + ["\""] = function(field,head) + head.read_mode = true + end, + [","] = function(field,head) + local v = head:pop() + if not v then + args.handle_error("Attempted to append to output on stack underflow") + return + end + if (v > 0) and (v < 256) then + v = string.char(v) + else + v = "" + end + args.handle_output(v) + end, + ["."] = function(field,head) + local v = head:pop() + if not v then + args.handle_error("Attempted to append to output on stack underflow") + return + end + args.handle_output(v) + end, + ["?"] = function(field,head) + local ranInt = math.random(0,200) + local direction + if ranInt > 99 then + direction = -1 + else + direction = 1 + end + if math.fmod(ranInt,2) == 0 then + head.direction = { + x = direction, + y = 0 + } + else + head.direction = { + x = 0, + y = direction + } + end + end, + ["!"] = function(field,head) + local v = head:pop() + if not v then + warn_args(field,head) + return + end + if v == 0 then + head:push(1) + else + head:push(0) + end + end, + ["`"] = function(field,head) + local a,b = head:pop(),head:pop() + if not (a or b) then + warn_args(field,head) + return + end + if a < b then + head:push(1) + else + head:push(0) + end + end, + ["|"] = function(field,head) + local v = head:pop() + if not v then + warn_args(field,head) + return + end + if v == 0 then + head.direction = { + x = 0, + y = 1 + } + else + head.direction = { + x = 0, + y = -1 + } + end + end, + ["_"] = function(field,head) + local v = head:pop() + if not v then + warn_args(field,head) + return + end + if v == 0 then + head.direction = { + x = 1, + y = 0 + } + else + head.direction = { + x = -1, + y = 0 + } + end + end, + [":"] = function(field,head) + local value = head:pop() + if not value then + warn_args(field,head) + value = 0 + end + head:push(value) + head:push(value) + end, + ["\\"] = function(field,head) + local a,b = head:pop(),head:pop() + if not (a or b) then + warn_args(field,head) + a = 0 + b = 0 + end + head:push(a) + head:push(b) + end, + ["#"] = function(field,head) + head:walk() + end, + ["&"] = function(field,head) + local v = args.handle_int_input() + if not v then + v = -1 + end + head:push(v) + end, + ["~"] = function(field,head) + local v = args.handle_input() + if not v then + v = -1 + end + head:push(v) + end, + ["p"] = function(field,head) + local y,x = head:pop(),head:pop() + local value = head:pop() + if not (y or x or value) then + args.handle_error("Attempted to call \"put\" operator with less than 3 arguments") + return + end + y,x = y+1,x+1 + if (not field[y]) or (not field[y][x]) then + if (y < field.max_field_height) and (x < field.max_field_width) and (x >= 0) and (y >= 0) then + field:expand(y,x) + else + args.handle_error("Attempted to get a value out of bounds") + return + end + end + field[y][x] = to_befunge_unit(value) + end, + ["g"] = function(field,head) + local y,x = head:pop(),head:pop() + if not (y or x) then + args.handle_error("Attempted to call \"get\" operator with less than 3 arguments") + return + end + y,x = y+1,x+1 + if (not field[y]) or (not field[y][x]) then + head:push(0) + args.handle_error("Attempted to put a value out of bounds") + return + end + head:push(from_befunge_unit(field[y][x])) + end, + ["$"] = function(field,head) + head:pop() + end + } + for I = 0,9 do + commands[tostring(I)] = function(field,head) + head:push(I) + end + end + local walker = rw_head(new_field,commands) + function befunge:run() + while befunge.interpreter_state do + walker:execute() + walker:walk() + if args.opcount and opcount > args.opcount then + args.handle_error("Instruction limit reached (>"..opcount..")") + break + end + opcount = opcount + 1 + end + return opcount + end +end + +return befunge diff --git a/libraries/esolangs/brainfuck.lua b/libraries/esolangs/brainfuck.lua new file mode 100644 index 0000000..17b6b6b --- /dev/null +++ b/libraries/esolangs/brainfuck.lua @@ -0,0 +1,217 @@ +local function find_bracket(text,left_br,right_br,start) + local top,pos,remaining_string = 0,0,text:sub(start,-1) + local match_start,match_end = remaining_string:find("["..left_br..right_br.."]") + while match_end do + pos = pos + match_end + if remaining_string:sub(match_end,match_end):match(left_br) then + top = top + 1 + elseif remaining_string:sub(match_end,match_end):match(right_br) then + top = top - 1 + end + if top == 0 then + break + end + remaining_string = remaining_string:sub(match_end+1,-1) + match_start,match_end = remaining_string:find("["..left_br..right_br.."]") + end + if match_end then + return pos+start-1 + else + return nil + end +end + +local function reverse_find_bracket(text,left_br,right_br,start) + local str = text:reverse() + local start = (string.len(text)-start) + v = find_bracket(str,right_br,left_br,start+1)-1 + v = text:len()-v + return v +end + +local bf = {} +--create interpreter object. created for each code chunk as tape reinitialization is required to run new code. +bf.new = function(input,options) + --assert input + assert(type(options) == "table" or type(options) == "nil","Expected argument #2 of type table, got "..type(options)) + assert(type(input) == "string","Expected argument #1 of type string, got "..type(input)) + + --generate the prototype and attach functions to it + local self = {tape = {}, pointer = 1, counter = 1, program = {}, stack = {}} + input:gsub(".",function(capt) self.program[#self.program + 1] = capt end) + setmetatable(self, {__index = bf}) + + --options and default values for them start here + if not options then options = {} end + self.options = {} + self.options.tapesize = options.tapesize or 30000 + self.options.dump_char = options.dump_char or "$" --extension lv3 goes against debug conventions. + self.options.debug_char = options.debug_char or "%" + self.options.dump_capacity = options.dump_capacity or 30 + self.options.debug = options.debug or false + self.options.path = options.path or "." + self.oplimit = options.limit or math.huge + if err then error(err) end + --store tokens for use during the main loop. + self.tokens = {} + self.tokens[">"] = function() + self.pointer = self.pointer + 1 + --wrap around + if self.tape[self.pointer] == nil then + self.pointer = 1 + end + end + self.tokens["<"] = function() + self.pointer = self.pointer - 1 + --wrap around + if self.tape[self.pointer] == nil then + self.pointer = #self.tape + end + end + self.tokens["+"] = function() + self.tape[self.pointer] = self.tape[self.pointer] + 1 + if self.tape[self.pointer] > 255 then + self.tape[self.pointer] = 0 + end + end + self.tokens["-"] = function() + self.tape[self.pointer] = self.tape[self.pointer] - 1 + if self.tape[self.pointer] < 0 then + self.tape[self.pointer] = 255 + end + end + self.tokens["."] = function() + self.output = self.output..string.char(self.tape[self.pointer]) + end + self.tokens[","] = function() + if self.input[1] then + self.tape[self.pointer] = self.input[1]:byte() + table.remove(self.input,1) + else + self.tape[self.pointer] = 0 + end + end + self.tokens["["] = function() + if self.tape[self.pointer] == 0 then + local count = find_bracket(table.concat(self.program),"%[","%]",self.counter) + if not count then self.tokens["@"](); self.errmsg = "structure error at "..self.pointer else + self.counter = count + end + end + end + self.tokens["]"] = function() + if self.tape[self.pointer] ~= 0 then + local count = reverse_find_bracket(table.concat(self.program),"%[","%]",self.counter) + if not count then self.tokens["@"](); self.errmsg = "structure error at "..self.pointer else + self.counter = count + end + end + end + self.tokens["@"] = function() + self.program[self.counter+1] = nil + end + --start debug tokens + if self.options.debug then + + --enable debugging flag + self.tokens[self.options.debug_char] = function() + self.debug = (not self.debug) + end + + --dump current stack and a short range of memory around the pointer to the console. + self.tokens[self.options.dump_char] = function() + local pointer = self.pointer - self.options.dump_capacity + if pointer < 0 then pointer = 0 end + local dump = "cell #"..pointer..": " + for I = 1,self.options.dump_capacity*2 do + local cell = self.tape[I+pointer] + if cell/100 < 1 then + if cell/10 < 1 then + cell = "00"..cell + else + cell = "0"..cell + end + end + dump = dump.." "..cell + end + dump = dump.."\nStack:\n" + for k,v in pairs(self.stack) do + local cell = v + if cell/100 < 1 then + if cell/10 < 1 then + cell = "00"..cell + else + cell = "0"..cell + end + end + dump = dump.." "..cell + end + self.output =self.output.."\n"..dump.."\n" + end + end + + --generate tape + for I = 1,self.options.tapesize do + self.tape[I] = 0 + end + --set reinitialization flags to false (no reinitialization before running) + self.init = false + self.default_program = self.program + return self +end + + +--execute the main loop. +bf.run = function(self,input) + --reinitialize tape, program, pointers and storages. + if self.init then + for I = 1,self.options.tapesize do + self.tape[I] = 0 + end + self.storage = 0 + self.stack = {} + --reset the EXT3/EXT2 self-modifications. + self.program = self.default_program + self.counter = 1 + self.pointer = 1 + end + + + --assert that self is an object. + assert(type(self) == "table") + --we can use tables of tokens instead of strings. + assert(type(input) == "string" or type(input) == "table", "Expected argument of type table or string, got "..type(input)) + if type(input) == "string" then + local temp_table = {} + input:gsub(".",function(capt) temp_table[#temp_table + 1] = capt end) + self.input = temp_table + end + local prgcount = 0 + self.output = "" + --[2] + + + while self.program[self.counter] ~= nil do + if self.tokens[self.program[self.counter]] then + self.tokens[self.program[self.counter]]() + prgcount = prgcount+1 --counts the total amount of operations. useful during debugging. + if self.debug then + self.output =self.output.."\n".."cycle: "..prgcount.."; token: "..self.program[self.counter]..", "..self.counter.."; memory: #"..self.pointer..", "..self.tape[self.pointer] + print("cycle: "..prgcount.."; token: "..self.program[self.counter]..", "..self.counter.."; memory: #"..self.pointer..", "..self.tape[self.pointer]) + if self.options.ext1 then + self.output =self.output.."; storage: "..self.storage + end + self.output =self.output.."\n" + end + if self.oplimit and prgcount > self.oplimit then + self.errmsg = "operation limit reached" + break + end + end + self.counter = self.counter + 1 --[1] + end + self.init = true + return self.output,prgcount,self.errmsg +end + +return bf diff --git a/libraries/eventlist.lua b/libraries/eventlist.lua new file mode 100644 index 0000000..1a46437 --- /dev/null +++ b/libraries/eventlist.lua @@ -0,0 +1,30 @@ + +--a list of all events that can arrive, for conveniennce +return {"clock","ready", +"serverLoaded","serverCommandPoolUpdate", +"serverSaveConfig", +"shardReady","onBotLocalSync", +"onBotReady","onBotLocalSave", +"shardResumed","channelCreate", +"channelUpdate","channelDelete", +"recipientAdd","recipientRemove", +"guildAvailable","guildCreate", +"guildUpdate","guildUnavailable", +"guildDelete","userBan", +"userUnban","emojisUpdate", +"memberJoin","memberLeave", +"memberUpdate","roleCreate", +"roleUpdate","roleDelete", +"messageCreate","messageUpdate", +"messageUpdateUncached","messageDelete", +"messageDeleteUncached","reactionAdd", +"reactionAddUncached","reactionRemove", +"reactionRemoveUncached","pinsUpdate", +"presenceUpdate","relationshipUpdate", +"relationshipAdd","relationshipRemove", +"typingStart","userUpdate", +"voiceConnect","voiceDisconnect", +"voiceUpdate","voiceChannelJoin", +"voiceChannelLeave","webhooksUpdate","debug", +"info","error","heartbeat","raw"} + diff --git a/libraries/file.lua b/libraries/file.lua new file mode 100644 index 0000000..b349bff --- /dev/null +++ b/libraries/file.lua @@ -0,0 +1,113 @@ +--This bot is heavily dependent on file operations, therefore this library exists. +local file = {} +local json +if pcall(import,"json") then + json = import("json") +elseif pcall(require,"json") then + json = require("json") +end +file.safe = true +file.read = function(filename,mode) + assert(type(filename) == "string","string expected, got "..type(filename)) + local mode = mode or "*a" + local temp_file,err = io.open(filename,r) + if err then + if not file.safe then error(err) else + ret_string = "" + end + else + ret_string = temp_file:read(mode) + temp_file:close() + end + return ret_string,err +end + +file.write = function(filename,write) + assert(type(filename) == "string", "string expected, got "..type(filename)) + assert(type(write) == "string", "string expected for argument #2, got "..type(write)) + local temp_file,err = io.open(filename,"w+") + local status = false + if err then + if not file.safe then error(err) else + status = false + end + else + temp_file:write(write) + temp_file:close() + status = true + end + return status,err +end + +file.exists = function(filename) + local file = io.open(filename,"r") + if file then + file:close() + return true + else + return false + end +end + +file.existsDir = function(filename) + local file = io.open(filename.."/stuff","w") + if file then + file:close() + os.remove(filename.."/stuff") + return true + else + return false + end +end + +file.ls = function(path) + if file.existsDir(path) then + local ls_handle = io.popen("ls -1 "..path,"r") + local ls_data = ls_handle:read("*a") + ls_handle:close() + return ls_data + else + return false, "No such file or directory" + end +end + +if json then + file.readJSON = function(filename,default) + assert(type(filename) == "string","string expected, got "..type(filename)) + local json_data,err = file.read(filename,"*a") + local table_data, status + if err then + if not file.safe then error(err) else + status = err + table_data = default or {} + end + else + table_data,_,err = json.decode(json_data) + if not table_data then + if not file.safe then error(err) else + status = err + table_data = default or {} + end + end + end + return table_data, status + end + file.writeJSON = function(filename,table_data) + assert(type(filename) == "string","string expected, got "..type(filename)) + assert(type(table_data) == "table","table expected, got "..type(table_data)) + local status = false + local status,json_object,_,err = pcall(function() return json.encode(table_data) end) + if not status then + if not file.safe then error(err) else + status = false + err = json_object + end + else + if json_object then + status,err = file.write(filename,json_object) + end + end + return status, err + end +end +return file diff --git a/libraries/import.lua b/libraries/import.lua new file mode 100644 index 0000000..4cbc416 --- /dev/null +++ b/libraries/import.lua @@ -0,0 +1,47 @@ +--Luvit's deadly sin - a library that fixes the dumbest problem in luvit +--That is, unability to load core modules from the files that were required +return function(reqfunc) + local function import(path) + local paths = { } + package.path:gsub("[^;]+",function(path) + table.insert(paths,path) + end) + local filename = path:gsub("%.","/") + local file = io.open(filename..".lua","r") + local iterator = 0 + local last_filename = "" + while not file do + iterator = iterator + 1 + if paths[iterator] then + file = io.open(paths[iterator]:gsub("%?",filename),"r") + last_filename = paths[iterator] + else + break + end + end + if not file then + return reqfunc(path) + else + content = file:read("*a") + local func,err = load(content,"import: "..filename,nil,setmetatable({ + require = reqfunc, + import = import, + },{__index = _G})) + if err then + error(err) + end + return func() + end + end + return import +end +--[[ +Usage: +import = require("import")(require) +file = import("file") + +yes, THAT easy. moreover, once you have imported the import function, it will be passed +to the loaded libraries. + +how hard is that to implement this but with the luvit's require, eh? +]] diff --git a/libraries/markov.lua b/libraries/markov.lua new file mode 100644 index 0000000..debe1fe --- /dev/null +++ b/libraries/markov.lua @@ -0,0 +1,122 @@ +local markov = {} + +local function node(relations) + local node = {} + local total = 0 + for k,v in pairs(relations) do + total = total + v.occurences + end + for k,v in pairs(relations) do + node[k] = {probability = v.occurences/total,occurences = v.occurences} + end + return node +end + +local function escape(str) + return str:gsub("([%%%*%(%)%^%.%[%]%+%-%$%?])","%%%1") +end + +local function register_words(str,word_list) + local word_list = word_list or {} + str:gsub("%S+",function(word) + if not word_list[word] then + word_list[word] = {} + end + local current_word = word_list[word] + str:gsub(escape(word) .. "%s+(%S+)",function(word2) + if not current_word[word2] then + current_word[word2] = {} + end + if not current_word[word2].occurences then + current_word[word2].occurences = 1 + else + current_word[word2].occurences = current_word[word2].occurences + 1 + end + end) + end) + for k,v in pairs(word_list) do + word_list[k] = node(v) + end + return word_list +end + +local table_length = function(tab) + local len = 0 + for k,v in pairs(tab) do + len = len + 1 + end + return len +end + +function markov.walk(self,start) + if not self.init then + error("Attempted to use an instance method on an uninitialized instance") + end + local random = math.random(0,1e7)/1e7 + local words = {} + words.count = 0 + local word = nil + if self.net[start] then + while (words.count < 1) and (table_length(self.net[start]) > 0) do + for k,v in pairs(self.net[start]) do + if (random <= v.probability) then + words.count = words.count + 1 + table.insert(words,k) + end + end + random = math.random(0,1e7)/1e7 + end + end + if words.count > 0 then + word = words[math.random(1,#words)] + end + return word +end + +function markov.expand_vocabulary(self,source) + if not self.init then + error("Attempted to use an instance method on an uninitialized instance") + end + self.net = register_words(source,self.net) +end + +function markov.save_state(self) + return self.net +end + +function markov.load_state(self,new_state) + self.net = new_state +end + +function markov.run(self,start,count) + if not self.init then + error("Attempted to use an instance method on an uninitialized instance") + end + if not start then + for k,v in pairs(self.net) do + start = k + break + end + end + local sequence = "" + local current_word = start + while current_word do + sequence = sequence..current_word.." " + local _,counter = sequence:gsub("(%S+)","%1") + current_word = self:walk(current_word) + if counter > (count or 200) then + sequence = sequence:sub(1,-2).."..." + break + end + end + return sequence +end + +function markov.new(str) + local self = setmetatable({},{__index = markov}) + self.net = register_words(str or "") + self.init = true + return self +end + +return markov diff --git a/libraries/purify.lua b/libraries/purify.lua new file mode 100644 index 0000000..1294730 --- /dev/null +++ b/libraries/purify.lua @@ -0,0 +1,32 @@ +--string purifier library +local purify = {} +purify.purify_pings = function(msg,input) + local text = input + while text:match("<@(%D*)(%d*)>") do + local obj,id = text:match("<@(%D*)(%d*)>") + local substitution = "" + if obj:match("!") then + local member = msg.guild:getMember(id) + if member then + substitution = "@"..member.name + end + elseif obj:match("&") then + local role = msg.guild:getRole(id) + if role then + substitution = "@"..role.name + end + end + if substitution == "" then + substitution = "<\\@"..obj..id..">" + end + text = text:gsub("<@(%D*)"..id..">",substitution) + end + return text +end + +purify.purify_escapes = function(text) + local match = "([%(%)%.%%%+%-%*%?%[%]%^%$])" + return text:gsub(match,"%%%1") +end + +return purify diff --git a/libraries/table-utils.lua b/libraries/table-utils.lua new file mode 100644 index 0000000..240e2f3 --- /dev/null +++ b/libraries/table-utils.lua @@ -0,0 +1,85 @@ +local utilities = {} +utilities.deepcopy = function(orig) + local orig_type = type(orig) + local copy + local depth = depth + if orig_type == 'table' then + copy = {} + for orig_key, orig_value in next, orig, nil do + copy[utilities.deepcopy(orig_key)] = utilities.deepcopy(orig_value) + end + setmetatable(copy, utilities.deepcopy(getmetatable(orig))) + else -- number, string, boolean, etc + copy = orig + end + return copy +end +utilities.slice = function(list,start,list_end) + local output = {} + for I = (start or 1),(list_end or #table) do + table.insert(output,list[I]) + end + return output +end +utilities.shallowcopy = function(orig) + local copy = {} + for k,v in pairs(orig) do + copy[k] = v + end + return copy +end +--overwrite the original table's properties with new properties +utilities.overwrite = function(original,overwrite) + local new = utilities.shallowcopy(original) + for k,v in pairs(overwrite) do + new[k] = v + end + return new +end +--merge all objects passed as arguments into a table. +--if the object is a table, merge all of it's contents with the table +utilities.merge = function(...) + local args = {...} + local new = {} + for k,v in pairs(args) do + if type(v) == "table" then + for k2,v2 in pairs(v) do + table.insert(new,v2) + end + else + table.insert(new,v) + end + end + return new +end +utilities.remove_value = function(tb,v) + local id_to_remove = nil + for k,f in pairs(tb) do + if f == v then + id_to_remove = k + end + end + if id_to_remove then + table.remove(tb,id_to_remove) + return true + else + return false + end +end +utilities.count = function(tb) + local count = 0 + for k,v in pairs(tb) do + count = count + 1 + end + return count +end +utilities.exists = function(tb,vc) + local kout + for k,v in pairs(tb) do + if v == vc then + kout = k + end + end + return kout +end +return utilities diff --git a/libraries/tasklib.lua b/libraries/tasklib.lua new file mode 100644 index 0000000..49e3a47 --- /dev/null +++ b/libraries/tasklib.lua @@ -0,0 +1,38 @@ +local class = import("classes.baseclass") +local taskhandler = class("TaskHandler") +local sqlite = import("sqlite3") +-- DB format: +-- +-- ID (INT) | event(STR) | args (STR) | date (STR) | command (STR) +-- ---------|------------|------------|------------|-------------- +-- 1 | time | NULL |12 30 1 10 | ?echo today is yes day +-- 2 | msg | hi |NULL | ?echo yes hello +-- +local exists = function(tab,sv) + for k,v in pairs(tab) do + if v == sv then + return true + end + end + return false +end +function taskhandler:__init(dbpath) + self.db = sqlite.open(dbpath) + local query = self.db:exec("SELECT * FROM sqlite_master;") + if not exists(query.tbl_name,"tasks") then + self.db:exec([[ +CREATE TABLE tasks( + ID INTEGER PRIMARY KEY, + event STR, + args STR, + date STR, + command STR +); + ]]) + end + self.cache = {} +end + +function taskhandler:daily_cache() + self.db: + diff --git a/libraries/unixToString.lua b/libraries/unixToString.lua new file mode 100644 index 0000000..056b0d0 --- /dev/null +++ b/libraries/unixToString.lua @@ -0,0 +1,9 @@ +return function(sec) + local hours = math.floor(sec/3600) + local minutes = math.floor((sec - hours*3600)/60) + local seconds = sec - (hours*3600) - (minutes*60) + hours = ((hours < 10) and ("0"..hours)) or hours + minutes = ((minutes < 10) and ("0"..minutes)) or minutes + seconds = ((seconds < 10) and ("0"..seconds)) or seconds + return ((tonumber(hours) > 0 and hours..":") or "")..minutes..":"..seconds +end diff --git a/plugins/debug/init.lua b/plugins/debug/init.lua new file mode 100644 index 0000000..4abc195 --- /dev/null +++ b/plugins/debug/init.lua @@ -0,0 +1,5 @@ +local plugin_c = import("classes.plugin") +local command = import("classes.command") +local plugin = plugin_c() + +return plugin diff --git a/plugins/esolang/help.lua b/plugins/esolang/help.lua new file mode 100644 index 0000000..d608ba4 --- /dev/null +++ b/plugins/esolang/help.lua @@ -0,0 +1,27 @@ +local discordia = require("discordia") +return { + ["brainfuck"] = {embed = { + title = "Run brainfuck code", + description = "specification can be found at https://esolangs.org/wiki/brainfuck", + color = discordia.Color.fromHex("#32cd32").value, + fields = { + {name = "Usage: ",value = "brainfuck []"}, + {name = "Perms: ",value = "all"}, + {name = "Options: ",value = [[ + -o; --output-only - print only the output, without an embed + ]]} + } + }}, + ["befunge"] = {embed = { + title = "Run befunge-93 code", + description = "specification can be found at https://esolangs.org/wiki/befunge", + fields = { + {name = "Usage: ",value = "befunge \\`\\`\\`\\`\\`\\` []"}, + {name = "Perms: ",value = "all"}, + {name = "Options: ",value = [[ +-o; --output-only - print only the output, without an embed + ]]} + }, + color = discordia.Color.fromHex("#32cd32").value, + }}, +} diff --git a/plugins/esolang/init.lua b/plugins/esolang/init.lua new file mode 100644 index 0000000..04aa629 --- /dev/null +++ b/plugins/esolang/init.lua @@ -0,0 +1,115 @@ +local brainfuck = require("esolangs.brainfuck") +local befunge = require("esolangs.befunge") +local plugin_class = import("classes.plugin") +local command = import("classes.command") +local plugin = plugin_class() +local settings = { + tapesize = 30000, + cellsize = 1, + debug = true, + limit = 500000 +} +c_brainfuck = command("brainfuck",{ + args = { + "string" + }, + exec = function(msg,args,opts) + settings.load_extensions = {} + settings.path = "./lib/brainfuck/" + local instance = brainfuck.new(args[1],settings) + local result,opcount,err = instance:run(args[2] or "") + if result == "" then + result = "" + end + if not err then + if opts["o"] or opts["output-only"] then + msg:reply(tostring(result):gsub("@","\\@")) + else + msg:reply({ embed = { + title = "Result:", + color = discordia.Color.fromHex("#32cd32").value, + description = "```"..tostring(result):gsub("`","\\`").." ```", + footer = { + text = "Finished in "..opcount.." operations" + } + }}) + end + else + msg:reply({ + embed = { + title = "Error:", + description = "```"..tostring(err).." ```", + color = discordia.Color.fromHex("#32cd32").value, + } + }) + end + end +}) +plugin:add_command(c_brainfuck) +c_befunge = command("befunge",{ + args = { + "string" + }, + exec = function(msg,args,opts) + local code = msg.content:match("```(.+)```") + if not code then + msg:reply("Invalid syntax") + return + end + local input = msg.content:match("```.+``` ?(.+)") or "" + local stdout = "" + local stderr = "" + befunge:init(code,{ + opcount = 10000, + handle_int_input = function() + local int = input:match("^%d+") + if not int then + return + end + input = input:gsub("^%d+","",1) + return tonumber(int) + end, + handle_input = function() + local char = input:sub(1,1) + if not char then + return + end + input = input:sub(2,-1) + return string.byte(char) + end, + handle_output = function(char) + stdout = stdout..char + end, + handle_warning = function(warn) + stderr = stderr.."[warning] "..warn.."\n" + end, + handle_error = function(error) + stderr = stderr.."[error] "..error.."\n" + befunge.interpreter_state = false + end + }) + local opcount = befunge:run() + if opts["o"] or opts["output-only"] then + msg:reply(tostring(stdout):gsub("@","\\@")) + else + msg:reply({embed = { + title = "Result: ", + color = discordia.Color.fromHex("#32cd32").value, + fields = { + {name = "out",value = "```"..stdout:gsub("`","\\`").." ```"}, + {name = "err",value = "```"..stderr.." ```"} + }, + footer = { + text = "Finished in "..opcount.." operations" + } + }}) + end + end +}) +plugin:add_command(c_befunge) +local helpdb = import(plugin_path:sub(3,-1).."help") +plugin:for_all_commands(function(command) + command:set_help(helpdb[command.name]) +end) + +return plugin diff --git a/plugins/help/init.lua b/plugins/help/init.lua new file mode 100644 index 0000000..b8525f5 --- /dev/null +++ b/plugins/help/init.lua @@ -0,0 +1,97 @@ +local pluginc = import("classes.plugin") +local command = import("classes.command") +local plugin = pluginc("help") +math.randomseed(os.time()+os.clock()) +local help_message +local function randomize_stuff() + local chance = math.random(1,100) + if chance < 10 then + help_message = [[ +This button here, builds Teleporters. This button, builds Dispensers. +And this little button makes them enemy sum-bitches wish they'd never been born! + +--the inspiration behind this bot's design ]] + elseif chance >= 10 and chance < 90 then + help_message = [[ +This plugin provides the help command, which can view help messages for plugins and commands + ]] + else + help_message = [[ +see the invisible +do the impossible +row row +fight da powah + ]] + end +end + +local function count(tab) + local count = 0 + for k,v in pairs(tab) do + count = count+1 + end + return count +end + +local function concatenate_keys(tab) + local key_list = {} + for k,v in pairs(tab) do + table.insert(key_list,k) + end + return "``"..table.concat(key_list,"``,\n``").."``" +end + +local help_command = command("help",{ + help = {embed={ + title = "View help embeds for commands and plugins", + description = "To specify if it's a plugin or a command, simply add the option accordingly", + fields = { + {name = "Usage:",value = "help [ or --plugin ]"}, + {name = "Perms:",value = "any"}, + {name = "Options:",value = "--plugin"} + } + }}, + exec = function(msg,args,opts) + randomize_stuff() + local embed = { + color = discordia.Color.fromHex("32b3bc").value + } + if args[1] then + if count(opts) < 1 then + if command_handler:get_command(args[1]) then + local command = command_handler:get_command(args[1]) + embed = command:get_help().embed + else + embed.description = "No such command" + end + elseif (opts["plugin"]) then + --[[ if plugin_data["plugins"] [args[1] ] then + embed.title = "Plugin ``"..args[1].."``:" + embed.description = plugin_data["plugins"] [args[1] ]["_help"] + embed.fields = {{ + name = "Commands:", + value ="``"..table.concat(plugin_data["plugins"] [args[1] ],"``,\n``").."``" + }} + else + embed.description = "No such plugin" + end + --]] + embed.title = "Not yet implemented" + embed.description = "Check again later" + end + else + embed.title = "SuppaBot-FW-r3 commands:" + embed.description = "use ``help `` to view help messages. (type ``help help`` for more info)" + embed.fields = {} + for k,v in pairs(command_handler:get_commands_metadata().plugins) do + table.insert(embed.fields,{ + name = k, + value = "``"..table.concat(v,"``, ``").."``" + }) + end + end + msg:reply({embed = embed}) + end, + }) +plugin:add_command(help_command) +return plugin diff --git a/plugins/meta/help.lua b/plugins/meta/help.lua new file mode 100644 index 0000000..8a3dd6c --- /dev/null +++ b/plugins/meta/help.lua @@ -0,0 +1,61 @@ +return { + ["prefix"] = {embed={ + title = "Set or view current prefix for this bot", + description = "If you're not sure what's the current prefix, just ping the bot", + fields = { + {name = "Usage:",value = "prefix [ or \"\"]"}, + {name = "Perms:",value = "Administrator"}, + } + }}, + ["alias"] = {embed={ + title = "Creates aliases", + description = "Add an alias for a command. (https://en.wikipedia.org/wiki/Alias_(command))", + fields = { + {name = "Usage: ",value = "alias \"\" \"\""}, + {name = "Examples: ",value = [[ +``alias !hi "!speak Hello!"`` - reply to !hi with "Hello!" using speak command +``alias !say "!speak ..."`` - reply to !hi with everything typed after !hi +``alias !say "!speak $1"`` - reply to !hi with the first argument sent along with !hi +More at https://github.com/yessiest/SuppaBot/wiki/Tasks]] + }, + {name = "Perms: ",value = "Administrator (doesn't apply to created aliases)"} + } + }}, + ["unalias"] = {embed = { + title = "Removes aliases", + description = "Remove a previously created alias", + fields = { + {name = "Usage: ",value = "unalias \"\""}, + {name = "Perms: ",value = "Administrator"} + } + }}, + ["aliases"] = {embed = { + title = "Lists aliases", + description = "List all previously created aliases", + fields = { + {name = "Usage: ",value = "aliases"}, + {name = "Perms: ",value = "all"} + } + }}, + ["ping"] = {embed = { + title = "View response latency", + description = "This command shows some latency stats", + fields = { + {name = "Usage: ",value = "ping"}, + {name = "Perms: ",value = "all"} + } + }}, + ["about"] = {embed = { + title = "View bot info", + description = "self-descriptive", + fields = { + {name = "Usage: ",value = "about"}, + {name = "Perms: ",value = "all"} + } + }}, + ["server"] = "Show server stats in a form of embed", + ["user"] = "View users stats", + ["speak"] = "Repeats the message, but suppresses the pings", + ["adminSpeak"] = "Repeats the message without suppressing pings (administrator permissions required)", + ["echo"] = "Repeats the message without deleting the command", +} diff --git a/plugins/meta/init.lua b/plugins/meta/init.lua new file mode 100644 index 0000000..3ad5239 --- /dev/null +++ b/plugins/meta/init.lua @@ -0,0 +1,348 @@ +segment = {} +segment.help = "This is a set of commands that add some features specific to this bot" + +local map = {} +local aliases = {} +local fake_message = import("fake_message") +local last_message_arrived = discordia.Stopwatch() +local unixToString = import("unixToString") +local command = import("classes.command") +local plugin = import("classes.plugin")("meta") + +client:on("messageCreate",function(msg) + last_message_arrived:reset() + last_message_arrived:start() +end) + +local prefix +for k,v in pairs(command_handler:get_prefixes()) do + if (not prefix) or prefix:len() > v:len() then + prefix = v + end +end + +local function add_alias(name,comm,prefix,description) + if (not map[name]) then + map[name] = {comm = comm,prefix = prefix} + aliases[name] = command(name,{ + help = "Alias for ``"..comm.."``", + usage = ((prefix and globals.prefix) or "")..name, + exec = function(msg,args2,opts) + local str = msg.content:gsub(name.." ","") + aftersub = comm:gsub("%.%.%.",str) + aftersub = aftersub:gsub("%$prefix",prefix) + local status,args = require("air").parse(str) + for k,v in pairs(args) do + aftersub = aftersub:gsub("([^\\])%$"..k,"%1"..v) + end + command_handler:handle(fake_message(msg,{ + content = aftersub + })) + end, + options = { + prefix = prefix, + custom = true + } + }) + plugin:add_command(aliases[name]) + return true + else + return false + end +end + +local function remove_alias(name) + if map[name] then + map[name] = nil + plugin:remove_command(aliases[name]) + return true + else + return false + end +end + +local function purify_strings(msg,input) + local text = input + while text:match("<@(%D*)(%d*)>") do + local obj,id = text:match("<@(%D*)(%d*)>") + local substitution = "" + if obj:match("!") then + local member = msg.guild:getMember(id) + if member then + substitution = "@"..member.name + end + elseif obj:match("&") then + local role = msg.guild:getRole(id) + if role then + substitution = "@"..role.name + end + end + if substitution == "" then + substitution = "<\\@"..obj..id..">" + end + text = text:gsub("<@(%D*)"..id..">",substitution) + end + text = text:gsub("@everyone","") + return text +end + +for k,v in pairs(import("file").readJSON("./servers/"..id.."/aliasmap.json",{})) do + commdata = v + if type(v) == "string" then --legacy format conversion + commdata = {comm = v, prefix = false} + end + add_alias(k,commdata.comm,commdata.prefix) +end + +local prefix = command("prefix",{ + help = "Set prefix", + usage = "prefix [(add | remove | list (default)) []]", + users = { + [client.owner.id] = 1 + }, + roles = { + ["747042157185073182"] = 1 + }, + perms = { + "administrator" + }, + exec = function(msg,args,opts) + local function list_prefixes(msg) + local prefixes = "" + for k,v in pairs(command_handler:get_prefixes()) do + prefixes = prefixes..v.."\n" + end + msg:reply({embed = { + title = "Prefixes for this server", + description = prefixes + }}) + end + if args[1] then + if args[1] == "add" and args[2] then + command_handler:add_prefix(args[2]) + msg:reply("Added "..args[2].." as a prefix") + elseif args[1] == "remove" and args[2] then + local status,err = command_handler:remove_prefix(args[2]) + if status then + msg:reply("Removed the "..args[2].." prefix") + else + msg:reply(err) + end + elseif args[1] == "list" then + list_prefixes(msg) + else + msg:reply("Syntax error") + end + else + list_prefixes(msg) + end + end +}) +plugin:add_command(prefix) + +local c_alias = command("alias", { + args = { + "string","string" + }, + perms = { + "administrator" + }, + exec = function(msg,args,opts) + if add_alias(args[1],args[2],(opts["prefix"] or opts["p"]),opts["description"]) then + msg:reply("Bound ``"..args[1].."`` as an alias to ``"..args[2].."``") + else + msg:reply("``"..args[1].."`` is already bound") + end + end +}) +plugin:add_command(c_alias) + +local c_unalias = command("unalias", { + args = { + "string" + }, + perms = { + "administrator" + }, + exec = function(msg,args,opts) + if remove_alias(args[1]) then + msg:reply("Removed the ``"..args[1].."`` alias") + else + msg:reply("No such alias") + end + end +}) +plugin:add_command(c_unalias) + +local c_aliases = command("aliases", { + exec = function(msg,args,opts) + msg:reply({embed = { + title = "Aliases for this server", + fields = (function() + local fields = {} + for k,v in pairs(map) do + table.insert(fields,{name = ((v["prefix"] and prefix) or "")..k,value = v["comm"]}) + end + return fields + end)() + }}) + end +}) +plugin:add_command(c_aliases) + +local c_ping = command("ping", { + exec = function(msg,args,opts) + local before = msg:getDate() + local reply = msg:reply("Pong!") + if not reply then + log("ERROR","Couldn't send the ping reply for some reason") + return + end + local after = reply:getDate() + local latency = (after:toMilliseconds() - before:toMilliseconds()) + last_message_arrived:stop() + local uptime = discordia.Date():toSeconds() - server.uptime:toSeconds() + local processing = (last_message_arrived:getTime():toMilliseconds()) + msg:reply({embed = { + title = "Stats:", + fields = { + {name = "Latency",value = tostring(math.floor(latency)).."ms"}, + {name = "Processing time",value = tostring(math.floor(processing)).."ms"}, + {name = "Uptime",value = tostring(unixToString(uptime))} + } + }}) + end +}) +plugin:add_command(c_ping) + +local c_about = command("about", { + exec = function(msg,args,opts) + local rand = math.random + local author = client:getUser("245973168257368076") + msg:reply({embed = { + title = "About Suppa-Bot", + thumbnail = { + url = client.user:getAvatarURL() + }, + color = discordia.Color.fromRGB(rand(50,200),rand(50,200),rand(50,200)).value, + description = "Suppa-Bot is an open-source bot written in Lua.", + fields = { + {name = "Source Code: ",value = "https://github.com/yessiest/SuppaBot"}, + {name = "Author: ",value = author.tag}, + {name = "Invite: ",value = "Not available yet"} + }, + footer = { + text = "This is a WIP port of the meta plugin from the old bot to the new framework" + } + }}) + end +}) +plugin:add_command(c_about) + +local c_server = command("server", { + exec = function(msg,args,opts) + msg:reply({embed = { + thumbnail = { + url = msg.guild.iconURL + }, + title = msg.guild.name, + description = msg.guild.description, + fields = { + {name = "Members",value = msg.guild.totalMemberCount,inline = true}, + {name = "Owner",value = (msg.guild.owner and msg.guild.owner.user.tag..":"..msg.guild.owner.user.id),inline = true}, + {name = "Created At",value = os.date("!%c",msg.guild.createdAt).." (UTC+0)",inline = true}, + {name = "Text Channels",value = msg.guild.textChannels:count(),inline = true}, + {name = "Voice Channels",value = msg.guild.voiceChannels:count(),inline = true} + } + }}) + end, +}) +plugin:add_command(c_server) + +local c_user = command("user", { + exec = function(msg,args,opts) + local member = msg.guild:getMember((args[1] or ""):match("%d+")) or msg.guild:getMember(msg.author.id) + local roles = "" + for k,v in pairs(member.roles) do + roles = roles..v.mentionString.."\n" + end + msg:reply({embed = { + title = member.user.tag..":"..member.user.id, + thumbnail = { + url = member.user:getAvatarURL() + }, + fields = { + {name = "Profile Created At",value = os.date("!%c",member.user.createdAt).." (UTC+0)"}, + {name = "Joined At",value = os.date("!%c",discordia.Date.fromISO(member.joinedAt):toSeconds()).." (UTC+0)",inline = true}, + {name = "Boosting",value = ((member.premiumSince and "Since "..member.premiumSince) or "No"),inline = true}, + {name = "Highest Role",value = member.highestRole.mentionString,inline = true}, + {name = "Roles",value = roles,inline = true} + } + }}) + end, +}) +plugin:add_command(c_user) + +local c_speak = command("speak", { + args = { + "string" + }, + exec = function(msg,args,opts) + local text = purify_strings(msg, table.concat(args," ")) + if opts["unescape"] or opts["u"] then + text = text:gsub("\\","") + end + msg:reply(text) + msg:delete() + end, +}) +plugin:add_command(c_speak) + +local c_adminSpeak = command("adminSpeak", { + args = { + "string" + }, + exec = function(msg,args,opts) + local text = table.concat(args," ") + if opts["unescape"] or opts["u"] then + text = text:gsub("\\","") + end + msg:reply(text) + msg:delete() + end, + perms = { + "mentionEveryone" + } +}) +plugin:add_command(c_adminSpeak) + +local c_echo = command("echo",{ + args = { + "string" + }, + exec = function(msg,args,opts) + local text = purify_strings(msg, table.concat(args," ")) + if opts["unescape"] or opts["u"] then + text = text:gsub("\\","") + end + msg:reply(text) + end, +}) +plugin:add_command(c_echo) + +plugin.removal_callback = function() + for k,v in pairs(map) do + remove_alias(k) + end +end + +local helpdb = import(plugin_path:sub(3,-1).."help") +plugin:for_all_commands(function(command) + command:set_help(helpdb[command.name]) +end) + +events:on("serverSaveConfig",function() + import("file").writeJSON("./servers/"..id.."/aliasmap.json",map) +end) + +return plugin diff --git a/plugins/plugins/init.lua b/plugins/plugins/init.lua new file mode 100644 index 0000000..0e522c9 --- /dev/null +++ b/plugins/plugins/init.lua @@ -0,0 +1,110 @@ +local plugin_c = import("classes.plugin") +local command = import("classes.command") +local plugin = plugin_c("pluginmanager") +local utilities = require("table-utils") + +local generic_admin_template = { + args = {"string"}, + perms = { + "administrator" + }, +} + +local enable = command("enable",utilities.overwrite(generic_admin_template,{ + help = {embed = { + title = "Enable plugin", + description = [[This command loads a plugin, +adding its commands to the command pool]], + fields = { + {name = "Usage:",value = "load "}, + {name = "Perms:",value = "Administrator, other (via ``rules --allow``)"} + }, + color = discordia.Color.fromHex("ff5100").value + }}, + exec = function(msg,args,opts) + local status,message = plugin_handler:load(args[1]) + local plugin_data = command_handler:get_commands_metadata().plugins + local embed = { + description = message, + color = discordia.Color.fromHex("ff5100").value, + } + if status then + embed.fields = { + {name = "New commands:",value = + table.concat(plugin_data[args[1]] or {},", ").." " + } + } + end + msg:reply({embed = embed}) + end +})) +plugin:add_command(enable) +local disable = command("disable",utilities.overwrite(generic_admin_template,{ + help = {embed = { + title = "Disable a loaded plugin", + description = [[This commands unloads a previously loaded plugin, +removing its commands from the command pool]], + fields = { + {name = "Usage:",value = "unload "}, + {name = "Perms:",value = "Administrator, other (via ``rules --allow``)"} + }, + color = discordia.Color.fromHex("ff5100").value + }}, + exec = function(msg,args,opts) + local plugin_data = command_handler:get_commands_metadata().plugins + if not (args[1] == "plugins") then + local status,message = plugin_handler:unload(args[1]) + local embed = { + description = message, + color = discordia.Color.fromHex("ff5100").value, + } + if status then + embed.fields = { + {name = "Removed commands:",value = + table.concat(plugin_data[args[1]] or {},", ").." " + } + } + end + msg:reply({embed = embed}) + else + msg:reply("TIME PARADOX") + end + end +})) +plugin:add_command(disable) +local plugins = command("plugins",utilities.overwrite(generic_admin_template,{ + help = {embed = { + title = "View all known plugins", + description = [[This commmand prints info on loaded and unloaded plugins]], + fields = { + {name = "Usage:",value = "plugins"}, + {name = "Perms:",value = "Administrator, other (via ``rules --allow``)"} + } + }}, + args = {}, + exec = function(msg,args,opts) + local all_plugins = plugin_handler:list_loadable() + local unloaded_plugins = {} + local loaded_plugins = {} + for k,v in pairs(all_plugins) do + if not v.loaded then + table.insert(unloaded_plugins,k) + else + table.insert(loaded_plugins,k) + end + end + if #unloaded_plugins == 0 then + table.insert(unloaded_plugins," ") + end + msg:reply({embed={ + color = discordia.Color.fromHex("ff5100").value, + fields = { + {name = "Loaded plugins",value = "``"..table.concat(loaded_plugins,"``,\n``").."``"}, + {name = "Unloaded plugins",value = "``"..table.concat(unloaded_plugins,"``,\n``").."``"} + } + }}) + end +})) +plugin:add_command(plugins) +return plugin + diff --git a/plugins/tools/init.lua b/plugins/tools/init.lua new file mode 100644 index 0000000..16ea5ff --- /dev/null +++ b/plugins/tools/init.lua @@ -0,0 +1,206 @@ +local plugin_class = import("classes.plugin") +local command = import("classes.command") +local plugin = plugin_class() +local markov = require("markov") +local markov_instance = markov.new() +math.randomseed(os.time()+os.clock()) + +local safe_clone = function(tab,disallow) + local new_tab = {} + for k,v in pairs(tab) do + if not disallow[k] then + new_tab[k] = v + end + end + return new_tab +end + +function to_bit_string(num) + local t="" + local rest + while num>0 do + rest=math.fmod(num,2) + t=t..rest + num=(num-rest)/2 + end + return t:reverse() +end + +local flip = command("flip",{ + help = "Flips a coin, obv.", + usage = "flip", + exec = function(msg,args,opts) + local coin = math.random(1,100)%2 + if coin > 0 then + msg:reply("Heads") + else + msg:reply("Tails") + end + end, +}) +plugin:add_command(flip) +local dice = command("dice",{ + help = "Simulates a dice throw, prints the value of each die", + usage = "dice <2d6,d30,d20+4,etc>", + exec = function(msg,args,opts) + local out = {embed = { + fields = {}, + footer = { + text = 0 + } + }} + for I = 1,#args do + local v = args[I] + for J = 1,(v:match("(%d+)d%d+") or 1) do + local value = math.random(1,tonumber(v:match("d(%d+)"))) + if v:find("d%d+[%+%-]%d+") then + if v:match("d%d+([%+%-])") == "+" then + value = value + tonumber(v:match("d%d+[%+%-](%d+)")) + else + value = value - tonumber(v:match("d%d+[%+%-](%d+)")) + end + end + out.embed.fields[#out.embed.fields+1] = {name = "d"..v:match("d(%d+)"),value = value, inline = true} + out.embed.footer.text = out.embed.footer.text+value + if #out.embed.fields >= 25 then + break + end + end + if #out.embed.fields >= 25 then + break + end + end + out.embed.footer.text = "Total: "..out.embed.footer.text + msg:reply(out) + end, +}) +plugin:add_command(dice) +local cards = command("cards",{ + help = "Draw a specific amount of playing cards and display them", + usage = "cards ", + args = {"number"}, + exec = function(msg,args,opts) + local out = {embed = { + fields = {} + }} + local random = math.random + for I = 1,(args[1] < 25 and args[1]) or 25 do + local suits = {"spades","clubs","diamonds","hearts"} + local values = { + "A","1","2","3","4","5", + "6","7","8","9","J","Q","K" + } + out.embed.fields[I] = {name = "card", value = " :"..suits[random(1,4)]..":"..values[random(1,11)].." ",inline = true} + end + msg:reply(out) + end, +}) +plugin:add_command(cards) +local calculate = command("calculate",{ + help = "Calculate maths using lua's interpeter. Math functions from C included, use ``sin(x)`` or ``cos(x)`` for example. Additionally, BitOp module is included with the name ``bit`` (example: ``bit.bnot(1,1)``)", + usage = [[ +calculate +``--bit``; ``-b`` - if the output is a number, convert it to binary +``--hex``; ``-h`` - if the output is a number, convert it to hexadecimal + ]], + args = { + "string" + }, + exec = function(msg,args,opts) + local calculation_coroutine = coroutine.wrap(function() + local sandbox = {} + sandbox = safe_clone(math,{randomseed = true}) + sandbox["bit"] = safe_clone(bit,{}) + local expression = (table.concat(args," ") or "") + local exception_keywords = { --this causes too much trouble + "while", + "function", + "for", + "if", + "then", + "do", + "end", + "repeat", + "until" + } + for k,v in pairs(exception_keywords) do + if expression:find("%W"..v.."%W") then + msg:reply("Invalid syntax") + return + end + end + local state,answer = pcall(load("return "..expression,"calc","t",setmetatable(sandbox,{}))) + if state then + if type(answer) == "number" then + if opts["bit"] or opts["b"] then + answer = "0b"..to_bit_string(answer) + elseif opts["hex"] or opts["h"] then + answer = "0x"..bit.tohex(answer) + end + end + msg:reply(tostring(answer)) + else + msg:reply(answer) + end + end) + calculation_coroutine() + end, +}) +plugin:add_command(calculate) +local pfp = command("pfp",{ + help = "Show the profile picture of a user, or if none is specified, of yourself", + usage = "pfp ", + exec = function(msg,args,opts) + local user = client:getUser((args[1] or ""):match("%d+")) + if user then + msg:reply(user:getAvatarURL().."?size=2048") + else + msg:reply(msg.author:getAvatarURL().."?size=2048") + end + end, +}) +plugin:add_command(pfp) +local markov = command("markov",{ + help = "Generate some text using markov chains", + usage = "markov ",[[ +--preset= - Select a text preset. Currently available: +``default`` - Generated from a wikipedia page on markov chains +``freud`` - The largest one, generated from a page on Sigmund Freud +``reddit`` - Generated from reddit comments +``travisscott`` - Generated from transcript of a video by PlasticPills on travis scott burger +]], + exec = function(msg,args,opts) + local preset,code,err = import("file").readJSON("./resources/"..(opts["preset"] or "default"):match("%w+")..".json",{system_failed = true}) + if preset.system_failed then + msg:reply("No such preset") + return + end + markov_instance:load_state(preset) + local output = markov_instance:run("The",100) + msg:reply(output) + end +}) +plugin:add_command(markov) +local embed = command("embed",{ + help = "Convert JSON objects into embeds", + usage = "If you've worked with discord.js before, this might be simple. If you haven't, then check out https://github.com/yessiest/SuppaBot/wiki/Embeds", + args = { + "string" + }, + exec = function(msg,args,opts) + local embed = msg.content:match("{.+}") + if not embed then + msg:reply("Invalid embed object") + return + end + local embed_obj,code,err = require("json").decode(embed) + if not embed_obj then + msg:reply("Error while decoding JSON object: "..tostring(err)) + return + end + embed_obj.color = discordia.Color.fromHex(embed_obj.color).value + msg:reply({embed = embed_obj}) + end +}) +plugin:add_command(embed) +return plugin diff --git a/resources/default.json b/resources/default.json new file mode 100644 index 0000000..028ba7c --- /dev/null +++ b/resources/default.json @@ -0,0 +1 @@ +{"discuss":{"Markov":{"probability":1,"occurences":1}},"Many":{"uses":{"probability":1,"occurences":1}},"future":{"actions":{"probability":0.5,"occurences":2},"states":{"probability":0.5,"occurences":2}},"Markov":{"chains;":{"probability":0.0625,"occurences":16},"property.":{"probability":0.25,"occurences":64},"chains":{"probability":0.25,"occurences":64},"chain":{"probability":0.3125,"occurences":80},"property,":{"probability":0.0625,"occurences":16},"chain,":{"probability":0.0625,"occurences":16}},"conditional":{"probability":{"probability":0.5,"occurences":2},"independence.":{"probability":0.5,"occurences":2}},"1.":{"In":{"probability":1,"occurences":1}},"arise":{"broadly":{"probability":1,"occurences":1}},"of)":{"future":{"probability":1,"occurences":1}},"(communication)":{"theory,":{"probability":1,"occurences":1}},"particular":{"state":{"probability":1,"occurences":1}},"all":{"that":{"probability":0.125,"occurences":2},"possible":{"probability":0.125,"occurences":2},"scores,":{"probability":0.125,"occurences":2},"is,":{"probability":0.125,"occurences":2},"color,":{"probability":0.125,"occurences":2},"is":{"probability":0.25,"occurences":4},"later":{"probability":0.125,"occurences":2}},"asks":{"what":{"probability":0.5,"occurences":2},"once":{"probability":0.5,"occurences":2}},"scores,":{"or":{"probability":1,"occurences":1}},"ball":{"later":{"probability":0.16666666666667,"occurences":5},"scores,":{"probability":0.16666666666667,"occurences":5},"is,":{"probability":0.16666666666667,"occurences":5},"is":{"probability":0.33333333333333,"occurences":10},"color,":{"probability":0.16666666666667,"occurences":5}},"stochastic":{"process":{"probability":0.6,"occurences":15},"properties":{"probability":0.2,"occurences":5},"process,":{"probability":0.2,"occurences":5}},"are":{"not":{"probability":0.16666666666667,"occurences":6},"focused":{"probability":0.16666666666667,"occurences":6},"widely":{"probability":0.16666666666667,"occurences":6},"fixed.":{"probability":0.16666666666667,"occurences":6},"many":{"probability":0.16666666666667,"occurences":6},"removed,":{"probability":0.16666666666667,"occurences":6}},"what":{"the":{"probability":1,"occurences":4}},"at":{"do":{"probability":0.076923076923077,"occurences":2},"is,":{"probability":0.15384615384615,"occurences":4},"led":{"probability":0.076923076923077,"occurences":2},"no":{"probability":0.076923076923077,"occurences":2},"the":{"probability":0.15384615384615,"occurences":4},"is":{"probability":0.076923076923077,"occurences":2},"its":{"probability":0.076923076923077,"occurences":2},"random":{"probability":0.076923076923077,"occurences":2},"experiences":{"probability":0.076923076923077,"occurences":2},"a":{"probability":0.076923076923077,"occurences":2},"of":{"probability":0.076923076923077,"occurences":2}},"X_1,":{"\\,":{"probability":1,"occurences":1}},"different.":{"A":{"probability":1,"occurences":1}},"therefore":{"time-inhomogeneous":{"probability":1,"occurences":1}},"change.":[],"sum":{"of":{"probability":1,"occurences":1}},"current":{"state":{"probability":0.5,"occurences":2},"state.":{"probability":0.5,"occurences":2}},"mathematical":{"system":{"probability":1,"occurences":1}},"a":{"labeled":{"probability":0.052631578947368,"occurences":19},"certain":{"probability":0.10526315789474,"occurences":38},"prolific":{"probability":0.052631578947368,"occurences":19},"general":{"probability":0.052631578947368,"occurences":19},"process":{"probability":0.052631578947368,"occurences":19},"finite":{"probability":0.052631578947368,"occurences":19},"Markov":{"probability":0.15789473684211,"occurences":57},"sequence":{"probability":0.052631578947368,"occurences":19},"way,":{"probability":0.052631578947368,"occurences":19},"stochastic":{"probability":0.15789473684211,"occurences":57},"mathematical":{"probability":0.052631578947368,"occurences":19},"ball":{"probability":0.052631578947368,"occurences":19},"bag":{"probability":0.052631578947368,"occurences":19},"time-homogeneous":{"probability":0.052631578947368,"occurences":19}},"same":{"question":{"probability":1,"occurences":1}},"matrix":{"methods.":{"probability":1,"occurences":1}},"necessary":{"to":{"probability":1,"occurences":1}},"any":{"vertex's":{"probability":0.14285714285714,"occurences":4},"size":{"probability":0.14285714285714,"occurences":4},"\"everyday\"":{"probability":0.14285714285714,"occurences":4},"uses":{"probability":0.14285714285714,"occurences":4},"state":{"probability":0.14285714285714,"occurences":4},"particular":{"probability":0.14285714285714,"occurences":4},"common":{"probability":0.14285714285714,"occurences":4}},"It":{"could":{"probability":1,"occurences":1}},"its":{"present":{"probability":1,"occurences":1}},"transitioning":{"to":{"probability":1,"occurences":1}},"finance.":{"While":{"probability":1,"occurences":1}},"machines,":{"and":{"probability":1,"occurences":1}},"begins":{"to":{"probability":1,"occurences":1}},"focused":{"on":{"probability":1,"occurences":1}},"color":{"ball":{"probability":0.5,"occurences":8},"for":{"probability":0.5,"occurences":8}},"be":{"modeled":{"probability":0.2,"occurences":5},"drastically":{"probability":0.2,"occurences":5},"\"memory-less.\"":{"probability":0.2,"occurences":5},"visualized":{"probability":0.2,"occurences":5},"anything:":{"probability":0.2,"occurences":5}},"important":{"precisely":{"probability":1,"occurences":1}},"led":{"the":{"probability":0.25,"occurences":1},"by":{"probability":0.25,"occurences":1},"up":{"probability":0.25,"occurences":1},"directed":{"probability":0.25,"occurences":1}},"sequence":{"X0, X1, X2, …X_0,":{"probability":1,"occurences":1}},"there":{"are":{"probability":1,"occurences":1}},"many":{"\"everyday\"":{"probability":0.5,"occurences":2},"common":{"probability":0.5,"occurences":2}},"That":{"is,":{"probability":1,"occurences":1}},"conditions,":{"baseball":{"probability":1,"occurences":1}},"drawn.":{"Once":{"probability":1,"occurences":1}},"also":{"ask":{"probability":1,"occurences":1}},"property.":{"While":{"probability":0.25,"occurences":4},"Depending":{"probability":0.25,"occurences":4},"Can":{"probability":0.25,"occurences":4},"A":{"probability":0.25,"occurences":4}},"replacement":{"each":{"probability":1,"occurences":1}},"state,":{"the":{"probability":1,"occurences":1}},"non-stationary":{"transition":{"probability":1,"occurences":1}},"number":{"of":{"probability":1,"occurences":1}},"bag":{"of":{"probability":1,"occurences":1}},"elapsed.":{"The":{"probability":1,"occurences":1}},"creates":{"a":{"probability":1,"occurences":1}},"rules.":{"The":{"probability":1,"occurences":1}},"weather":{"conditions,":{"probability":1,"occurences":1}},"\"memory-less.\"":{"That":{"probability":1,"occurences":1}},"game":{"theory,":{"probability":1,"occurences":1}},"than":{"the":{"probability":1,"occurences":1}},"that":{"no":{"probability":0.125,"occurences":8},"experiences":{"probability":0.125,"occurences":8},"do":{"probability":0.125,"occurences":8},"led":{"probability":0.125,"occurences":8},"of":{"probability":0.125,"occurences":8},"is,":{"probability":0.125,"occurences":8},"is":{"probability":0.125,"occurences":8},"a":{"probability":0.125,"occurences":8}},"cases":{"with":{"probability":1,"occurences":1}},"(steps":{"increase),":{"probability":1,"occurences":1}},"differs":{"from":{"probability":1,"occurences":1}},"process,":{"but":{"probability":0.5,"occurences":2},"however,":{"probability":0.5,"occurences":2}},"process":{"begins":{"probability":0.2,"occurences":5},"may":{"probability":0.2,"occurences":5},"in":{"probability":0.2,"occurences":5},"with":{"probability":0.2,"occurences":5},"arrived":{"probability":0.2,"occurences":5}},"again,":{"this":{"probability":1,"occurences":1}},"called":{"the":{"probability":1,"occurences":1}},"chains;":{"that":{"probability":1,"occurences":1}},"out":{"why?":{"probability":1,"occurences":1}},"chain,":{"in":{"probability":1,"occurences":1}},"question":{"asks":{"probability":1,"occurences":4}},"time-inhomogeneous":{"Markov":{"probability":1,"occurences":1}},"anything:":{"letters,":{"probability":1,"occurences":1}},"the":{"same":{"probability":0.03125,"occurences":31},"initial":{"probability":0.03125,"occurences":31},"Markov":{"probability":0.15625,"occurences":155},"rule":{"probability":0.03125,"occurences":31},"possible":{"probability":0.03125,"occurences":31},"one":{"probability":0.03125,"occurences":31},"language":{"probability":0.03125,"occurences":31},"current":{"probability":0.0625,"occurences":62},"next":{"probability":0.03125,"occurences":31},"probability":{"probability":0.25,"occurences":248},"steps":{"probability":0.03125,"occurences":31},"process":{"probability":0.03125,"occurences":31},"sum":{"probability":0.03125,"occurences":31},"theory":{"probability":0.03125,"occurences":31},"labels":{"probability":0.03125,"occurences":31},"random":{"probability":0.0625,"occurences":62},"previous":{"probability":0.03125,"occurences":31},"present":{"probability":0.03125,"occurences":31},"most":{"probability":0.03125,"occurences":31}},"rule":{"of":{"probability":1,"occurences":1}},"why?":{"In":{"probability":1,"occurences":1}},"by":{"finite":{"probability":1,"occurences":1}},"must":{"be":{"probability":1,"occurences":1}},"states":{"are":{"probability":1,"occurences":1}},"definition":{"is":{"probability":1,"occurences":1}},"matter":{"how":{"probability":1,"occurences":1}},"The":{"state":{"probability":0.5,"occurences":2},"defining":{"probability":0.5,"occurences":2}},"of":{"any":{"probability":0.08,"occurences":50},"their":{"probability":0.04,"occurences":25},"the":{"probability":0.2,"occurences":125},"state":{"probability":0.04,"occurences":25},"getting":{"probability":0.08,"occurences":50},"states.":{"probability":0.04,"occurences":25},"Markov":{"probability":0.08,"occurences":50},"all":{"probability":0.04,"occurences":25},"moving":{"probability":0.04,"occurences":25},"multi-colored":{"probability":0.04,"occurences":25},"conditional":{"probability":0.08,"occurences":50},"time.":{"probability":0.04,"occurences":25},"random":{"probability":0.04,"occurences":25},"transitioning":{"probability":0.04,"occurences":25},"a":{"probability":0.08,"occurences":50},"stochastic":{"probability":0.04,"occurences":25}},"visualized":{"with":{"probability":1,"occurences":1}},"other":{"words,":{"probability":0.5,"occurences":4},"according":{"probability":0.25,"occurences":2},"may":{"probability":0.25,"occurences":2}},"infinite)":{"number":{"probability":1,"occurences":1}},"broader":{"than":{"probability":1,"occurences":1}},"require":{"proficiency":{"probability":1,"occurences":1}},"variables,":{"a":{"probability":1,"occurences":1}},"their":{"usefulness":{"probability":1,"occurences":1}},"does":{"not":{"probability":0.5,"occurences":2},"satisfy":{"probability":0.5,"occurences":2}},"in":{"probabilistic":{"probability":0.071428571428571,"occurences":5},"color":{"probability":0.14285714285714,"occurences":10},"must":{"probability":0.071428571428571,"occurences":5},"is":{"probability":0.28571428571429,"occurences":20},"statistical":{"probability":0.071428571428571,"occurences":5},"economics,":{"probability":0.071428571428571,"occurences":5},"which":{"probability":0.071428571428571,"occurences":5},"that":{"probability":0.071428571428571,"occurences":5},"mathematics.":{"probability":0.071428571428571,"occurences":5},"for":{"probability":0.071428571428571,"occurences":5}},"usefulness":{"in":{"probability":1,"occurences":1}},"not":{"dependent":{"probability":0.33333333333333,"occurences":3},"satisfy":{"probability":0.66666666666667,"occurences":6}},"can":{"be":{"probability":1,"occurences":1}},"time.":{"Such":{"probability":1,"occurences":1}},"baseball":{"scores,":{"probability":1,"occurences":1}},"you":{"figure":{"probability":1,"occurences":1}},"probabilistic":{"rules.":{"probability":1,"occurences":1}},"broadly":{"in":{"probability":1,"occurences":1}},"space,":{"the":{"probability":0.5,"occurences":2},"or":{"probability":0.5,"occurences":2}},"when":{"selecting":{"probability":1,"occurences":1}},"labels":{"of":{"probability":1,"occurences":1}},"language":{"of":{"probability":1,"occurences":1}},"In":{"the":{"probability":0.2,"occurences":5},"such":{"probability":0.2,"occurences":5},"probability":{"probability":0.2,"occurences":5},"other":{"probability":0.4,"occurences":10}},"dependent":{"solely":{"probability":0.33333333333333,"occurences":2},"upon":{"probability":0.33333333333333,"occurences":2},"of":{"probability":0.33333333333333,"occurences":2}},"upon":{"the":{"probability":0.5,"occurences":2},"which":{"probability":0.5,"occurences":2}},"Can":{"you":{"probability":1,"occurences":1}},"words,":{"the":{"probability":0.5,"occurences":2},"knowledge":{"probability":0.5,"occurences":2}},"possible":{"states,":{"probability":0.33333333333333,"occurences":3},"future":{"probability":0.33333333333333,"occurences":3},"to":{"probability":0.33333333333333,"occurences":3}},"size":{"of":{"probability":1,"occurences":1}},"or":{"for":{"probability":0.2,"occurences":4},"countably":{"probability":0.1,"occurences":2},"set":{"probability":0.1,"occurences":2},"stock":{"probability":0.1,"occurences":2},"ball":{"probability":0.3,"occurences":6},"which":{"probability":0.1,"occurences":2},"non-stationary":{"probability":0.1,"occurences":2}},"finite":{"state":{"probability":0.5,"occurences":2},"(or":{"probability":0.5,"occurences":2}},"theory,":{"queueing":{"probability":0.33333333333333,"occurences":3},"genetics,":{"probability":0.33333333333333,"occurences":3},"the":{"probability":0.33333333333333,"occurences":3}},"genetics,":{"and":{"probability":1,"occurences":1}},"multi-colored":{"balls.":{"probability":1,"occurences":1}},"ask":{"what":{"probability":1,"occurences":1}},"with":{"any":{"probability":0.16666666666667,"occurences":6},"common":{"probability":0.16666666666667,"occurences":6},"a":{"probability":0.33333333333333,"occurences":12},"color":{"probability":0.33333333333333,"occurences":12}},"balls":{"are":{"probability":1,"occurences":1}},"Such":{"a":{"probability":1,"occurences":1}},"allows":{"replacement":{"probability":0.5,"occurences":2},"for":{"probability":0.5,"occurences":2}},"walks":{"provide":{"probability":1,"occurences":1}},"system":{"that":{"probability":1,"occurences":1}},"increase),":{"the":{"probability":1,"occurences":1}},"this":{"creates":{"probability":1,"occurences":1}},"set":{"of":{"probability":1,"occurences":1}},"experiences":{"transitions":{"probability":1,"occurences":1}},"above,":{"as":{"probability":1,"occurences":1}},"according":{"to":{"probability":1,"occurences":1}},"\\,":{"X_2,":{"probability":0.33333333333333,"occurences":3},"X_1,":{"probability":0.33333333333333,"occurences":3},"\\dotsX0​,X1​,X2​,…":{"probability":0.33333333333333,"occurences":3}},"distribution":{"of":{"probability":1,"occurences":1}},"letters,":{"numbers,":{"probability":1,"occurences":1}},"(the":{"probability":{"probability":1,"occurences":1}},"however,":{"does":{"probability":1,"occurences":1}},"widely":{"employed":{"probability":1,"occurences":1}},"from":{"one":{"probability":0.5,"occurences":8},"a":{"probability":0.5,"occurences":8}},"such":{"a":{"probability":1,"occurences":1}},"for":{"the":{"probability":0.4,"occurences":10},"ball":{"probability":0.2,"occurences":5},"which":{"probability":0.2,"occurences":5},"non-stationary":{"probability":0.2,"occurences":5}},"one":{"state":{"probability":0.66666666666667,"occurences":6},"explored":{"probability":0.33333333333333,"occurences":3}},"uses":{"of":{"probability":1,"occurences":1}},"another":{"according":{"probability":0.5,"occurences":2},"may":{"probability":0.5,"occurences":2}},"They":{"arise":{"probability":1,"occurences":1}},"economics,":{"game":{"probability":1,"occurences":1}},"fixed.":{"In":{"probability":1,"occurences":1}},"and":{"are":{"probability":0.1,"occurences":10},"it":{"probability":0.1,"occurences":10},"information-theoretical":{"probability":0.1,"occurences":10},"so":{"probability":0.1,"occurences":10},"therefore":{"probability":0.1,"occurences":10},"random":{"probability":0.2,"occurences":20},"time":{"probability":0.1,"occurences":10},"finance.":{"probability":0.1,"occurences":10},"most":{"probability":0.1,"occurences":10}},"explored":{"above,":{"probability":1,"occurences":1}},"no":{"matter":{"probability":1,"occurences":1}},"edges":{"is":{"probability":1,"occurences":1}},"knowledge":{"of":{"probability":1,"occurences":1}},"time":{"a":{"probability":0.33333333333333,"occurences":3},"goes":{"probability":0.33333333333333,"occurences":3},"elapsed.":{"probability":0.33333333333333,"occurences":3}},"time-homogeneous":{"Markov":{"probability":1,"occurences":1}},"labeled":{"directed":{"probability":1,"occurences":1}},"do":{"not":{"probability":1,"occurences":1}},"drastically":{"different.":{"probability":1,"occurences":1}},"goes":{"on":{"probability":1,"occurences":1}},"vertex's":{"outgoing":{"probability":1,"occurences":1}},"balls.":{"It":{"probability":1,"occurences":1}},"getting":{"a":{"probability":1,"occurences":4}},"moving":{"from":{"probability":1,"occurences":1}},"how":{"the":{"probability":1,"occurences":1}},"mathematics.":{"They":{"probability":1,"occurences":1}},"variables":{"satisfying":{"probability":1,"occurences":1}},"to":{"any":{"probability":0.125,"occurences":8},"exist":{"probability":0.125,"occurences":8},"determine":{"probability":0.125,"occurences":8},"discuss":{"probability":0.125,"occurences":8},"another":{"probability":0.25,"occurences":16},"the":{"probability":0.125,"occurences":8},"certain":{"probability":0.125,"occurences":8}},"variable,":{"and":{"probability":1,"occurences":1}},"X0, X1, X2, …X_0,":{"\\,":{"probability":1,"occurences":1}},"present":{"state,":{"probability":0.5,"occurences":2},"state.":{"probability":0.5,"occurences":2}},"variant":{"of":{"probability":1,"occurences":1}},"independence.":{"In":{"probability":1,"occurences":1}},"independent":{"of":{"probability":1,"occurences":1}},"theory":{"and":{"probability":0.5,"occurences":2},"of":{"probability":0.5,"occurences":2}},"exist":{"with":{"probability":1,"occurences":1}},"\"everyday\"":{"processes":{"probability":1,"occurences":1}},"prolific":{"example":{"probability":1,"occurences":1}},"characteristic":{"of":{"probability":1,"occurences":1}},"as":{"it":{"probability":0.5,"occurences":2},"time":{"probability":0.5,"occurences":2}},"certain":{"probabilistic":{"probability":0.33333333333333,"occurences":3},"color":{"probability":0.66666666666667,"occurences":6}},"immediate":{"example":{"probability":1,"occurences":1}},"applications":{"are":{"probability":1,"occurences":1}},"may":{"be":{"probability":0.75,"occurences":12},"change.":{"probability":0.25,"occurences":4}},"once":{"again":{"probability":1,"occurences":1}},"defining":{"characteristic":{"probability":1,"occurences":1}},"next":{"ball":{"probability":1,"occurences":1}},"is":{"that":{"probability":0.10526315789474,"occurences":32},"drawn.":{"probability":0.052631578947368,"occurences":16},"possible":{"probability":0.052631578947368,"occurences":16},"process,":{"probability":0.052631578947368,"occurences":16},"is":{"probability":0.052631578947368,"occurences":16},"important":{"probability":0.052631578947368,"occurences":16},"dependent":{"probability":0.052631578947368,"occurences":16},"all":{"probability":0.052631578947368,"occurences":16},"necessary":{"probability":0.052631578947368,"occurences":16},"broader":{"probability":0.052631578947368,"occurences":16},"independent":{"probability":0.052631578947368,"occurences":16},"definition":{"probability":0.052631578947368,"occurences":16},"1.":{"probability":0.052631578947368,"occurences":16},"when":{"probability":0.052631578947368,"occurences":16},"a":{"probability":0.15789473684211,"occurences":48},"creates":{"probability":0.052631578947368,"occurences":16}},"later":{"may":{"probability":1,"occurences":1}},"proficiency":{"with":{"probability":1,"occurences":1}},"X_2,":{"\\,":{"probability":1,"occurences":1}},"information-theoretical":{"contexts":{"probability":1,"occurences":1}},"so":{"ask":{"probability":0.33333333333333,"occurences":2},"on.":{"probability":0.33333333333333,"occurences":2},"many":{"probability":0.33333333333333,"occurences":2}},"because":{"so":{"probability":1,"occurences":1}},"general":{"stochastic":{"probability":1,"occurences":1}},"on.":{"In":{"probability":1,"occurences":1}},"could":{"also":{"probability":1,"occurences":1}},"determine":{"the":{"probability":1,"occurences":1}},"on":{"matrix":{"probability":0.071428571428571,"occurences":3},"cases":{"probability":0.071428571428571,"occurences":3},"is":{"probability":0.14285714285714,"occurences":6},"the":{"probability":0.14285714285714,"occurences":6},"examples":{"probability":0.071428571428571,"occurences":3},"probability":{"probability":0.071428571428571,"occurences":3},"probabilities":{"probability":0.071428571428571,"occurences":3},"which":{"probability":0.071428571428571,"occurences":3},"of":{"probability":0.071428571428571,"occurences":3},"(steps":{"probability":0.071428571428571,"occurences":3},"asks":{"probability":0.14285714285714,"occurences":6}},"A":{"Markov":{"probability":0.5,"occurences":8},"variant":{"probability":0.25,"occurences":4},"common":{"probability":0.25,"occurences":4}},"countably":{"infinite)":{"probability":1,"occurences":1}},"Once":{"again,":{"probability":1,"occurences":1}},"queueing":{"(communication)":{"probability":1,"occurences":1}},"modeled":{"by":{"probability":1,"occurences":1}},"performances.":{"Markov":{"probability":1,"occurences":1}},"methods.":{"A":{"probability":1,"occurences":1}},"again":{"for":{"probability":1,"occurences":1}},"example":{"of":{"probability":0.5,"occurences":2},"is":{"probability":0.5,"occurences":2}},"satisfy":{"the":{"probability":1,"occurences":16}},"actions":{"are":{"probability":1,"occurences":1}},"figure":{"out":{"probability":1,"occurences":1}},"(or":{"countably":{"probability":1,"occurences":1}},"arrived":{"at":{"probability":1,"occurences":1}},"up":{"to":{"probability":1,"occurences":1}},"steps":{"increase),":{"probability":0.5,"occurences":1},"that":{"probability":0.5,"occurences":1}},"processes":{"satisfy":{"probability":1,"occurences":1}},"state.":{"This":{"probability":1,"occurences":4}},"This":{"process,":{"probability":0.33333333333333,"occurences":3},"is":{"probability":0.33333333333333,"occurences":3},"definition":{"probability":0.33333333333333,"occurences":3}},"way,":{"a":{"probability":1,"occurences":1}},"probability":{"theory,":{"probability":0.090909090909091,"occurences":11},"of)":{"probability":0.090909090909091,"occurences":11},"of":{"probability":0.54545454545455,"occurences":66},"question":{"probability":0.090909090909091,"occurences":11},"and":{"probability":0.090909090909091,"occurences":11},"distribution":{"probability":0.090909090909091,"occurences":11}},"\\dotsX0​,X1​,X2​,…":{"of":{"probability":1,"occurences":1}},"statistical":{"and":{"probability":1,"occurences":1}},"property,":{"there":{"probability":1,"occurences":1}},"previous":{"state":{"probability":1,"occurences":1}},"solely":{"on":{"probability":1,"occurences":1}},"random":{"variable,":{"probability":0.16666666666667,"occurences":6},"walks":{"probability":0.16666666666667,"occurences":6},"from":{"probability":0.16666666666667,"occurences":6},"variable.":{"probability":0.16666666666667,"occurences":6},"variables":{"probability":0.16666666666667,"occurences":6},"variables,":{"probability":0.16666666666667,"occurences":6}},"but":{"it":{"probability":1,"occurences":4}},"common":{"matrix":{"probability":0.33333333333333,"occurences":3},"examples":{"probability":0.33333333333333,"occurences":3},"probability":{"probability":0.33333333333333,"occurences":3}},"is,":{"and":{"probability":0.33333333333333,"occurences":3},"as":{"probability":0.33333333333333,"occurences":3},"(the":{"probability":0.33333333333333,"occurences":3}},"selecting":{"uniformly":{"probability":1,"occurences":1}},"uniformly":{"at":{"probability":1,"occurences":1}},"numbers,":{"weather":{"probability":1,"occurences":1}},"initial":{"theory":{"probability":1,"occurences":1}},"chains":{"with":{"probability":0.25,"occurences":4},"require":{"probability":0.25,"occurences":4},"is":{"probability":0.25,"occurences":4},"may":{"probability":0.25,"occurences":4}},"removed,":{"the":{"probability":1,"occurences":1}},"outgoing":{"edges":{"probability":1,"occurences":1}},"properties":{"that":{"probability":1,"occurences":1}},"Depending":{"upon":{"probability":1,"occurences":1}},"examples":{"of":{"probability":1,"occurences":1}},"provide":{"a":{"probability":1,"occurences":1}},"state":{"space,":{"probability":0.22222222222222,"occurences":18},"transition":{"probability":0.11111111111111,"occurences":9},"and":{"probability":0.11111111111111,"occurences":9},"to":{"probability":0.22222222222222,"occurences":18},"is":{"probability":0.22222222222222,"occurences":18},"machines,":{"probability":0.11111111111111,"occurences":9}},"states.":{"Many":{"probability":1,"occurences":1}},"color,":{"but":{"probability":1,"occurences":1}},"chain":{"is":{"probability":0.8,"occurences":20},"must":{"probability":0.2,"occurences":5}},"variable.":{"This":{"probability":1,"occurences":1}},"precisely":{"because":{"probability":1,"occurences":1}},"each":{"time":{"probability":1,"occurences":1}},"contexts":{"and":{"probability":1,"occurences":1}},"transition":{"is":{"probability":0.5,"occurences":2},"probabilities":{"probability":0.5,"occurences":2}},"stock":{"performances.":{"probability":1,"occurences":1}},"directed":{"graph,":{"probability":1,"occurences":1}},"graph,":{"for":{"probability":1,"occurences":1}},"transitions":{"from":{"probability":1,"occurences":1}},"states,":{"can":{"probability":1,"occurences":1}},"employed":{"in":{"probability":1,"occurences":1}},"While":{"the":{"probability":0.5,"occurences":2},"it":{"probability":0.5,"occurences":2}},"probabilities":{"and":{"probability":1,"occurences":1}},"which":{"balls":{"probability":0.33333333333333,"occurences":3},"the":{"probability":0.66666666666667,"occurences":6}},"it":{"differs":{"probability":0.2,"occurences":5},"allows":{"probability":0.4,"occurences":10},"is":{"probability":0.2,"occurences":5},"does":{"probability":0.2,"occurences":5}},"satisfying":{"the":{"probability":1,"occurences":1}},"most":{"immediate":{"probability":0.5,"occurences":2},"applications":{"probability":0.5,"occurences":2}}} diff --git a/resources/freud.json b/resources/freud.json new file mode 100644 index 0000000..e7d3c60 --- /dev/null +++ b/resources/freud.json @@ -0,0 +1 @@ +{"where":{"Jung":{"probability":0.33333333333333,"occurences":3},"he":{"probability":0.33333333333333,"occurences":3},"they":{"probability":0.33333333333333,"occurences":3}},"Nietzsche's":{"The":{"probability":0.25,"occurences":4},"writings":{"probability":0.25,"occurences":4},"death,":{"probability":0.25,"occurences":4},"works":{"probability":0.25,"occurences":4}},"originate":{"to":{"probability":1,"occurences":1}},"Fliess,":{"that":{"probability":0.33333333333333,"occurences":3},"he":{"probability":0.33333333333333,"occurences":3},"a":{"probability":0.33333333333333,"occurences":3}},"linked":{"to":{"probability":1,"occurences":4}},"consulting":{"rooms":{"probability":1,"occurences":1}},"1887.":{"Both":{"probability":1,"occurences":1}},"Brill":{"given":{"probability":0.33333333333333,"occurences":3},"and":{"probability":0.33333333333333,"occurences":3},"founded":{"probability":0.33333333333333,"occurences":3}},"extraordinarius\"":{"was":{"probability":1,"occurences":1}},"audience.":{"Once":{"probability":1,"occurences":1}},"tissue":{"of":{"probability":1,"occurences":1}},"particular":{"patient":{"probability":1,"occurences":1}},"adopted":{"the":{"probability":1,"occurences":1}},"Jones,":{"then":{"probability":0.5,"occurences":2},"who":{"probability":0.5,"occurences":2}},"conjunction":{"with":{"probability":1,"occurences":1}},"mysticism\"":{"lay":{"probability":1,"occurences":1}},"unconscious":{"memories":{"probability":0.2,"occurences":5},"material":{"probability":0.2,"occurences":5},"and":{"probability":0.2,"occurences":5},"mind":{"probability":0.2,"occurences":5},"probably":{"probability":0.2,"occurences":5}},"childhood.":{"His":{"probability":1,"occurences":1}},"\"rearousing":{"[Freud's]":{"probability":1,"occurences":1}},"suggested":{"to":{"probability":0.5,"occurences":2},"that":{"probability":0.5,"occurences":2}},"masturbation,":{"\"the":{"probability":0.5,"occurences":2},"coitus":{"probability":0.5,"occurences":2}},"they":{"established":{"probability":0.125,"occurences":8},"were":{"probability":0.25,"occurences":16},"held":{"probability":0.125,"occurences":8},"became":{"probability":0.125,"occurences":8},"had":{"probability":0.125,"occurences":8},"left":{"probability":0.125,"occurences":8},"met":{"probability":0.125,"occurences":8}},"hysteria,":{"demonstrated":{"probability":1,"occurences":1}},"described":{"the":{"probability":1,"occurences":4}},"Birth":{"of":{"probability":1,"occurences":1}},"culpability":{"–":{"probability":1,"occurences":1}},"attempt":{"at":{"probability":1,"occurences":1}},"neurosis\",":{"and":{"probability":1,"occurences":1}},"nonetheless":{"continued":{"probability":1,"occurences":1}},"same":{"year.":{"probability":0.16666666666667,"occurences":6},"year":{"probability":0.33333333333333,"occurences":12},"year,":{"probability":0.33333333333333,"occurences":12},"secondary":{"probability":0.16666666666667,"occurences":6}},"Paris":{"on":{"probability":1,"occurences":1}},"Mittwochs-Gesellschaft)":{"and":{"probability":1,"occurences":1}},"Stadt,":{"a":{"probability":1,"occurences":1}},"1882,":{"Freud":{"probability":1,"occurences":1}},"whom":{"Freud":{"probability":0.25,"occurences":4},"were":{"probability":0.25,"occurences":4},"he":{"probability":0.5,"occurences":8}},"it,":{"he":{"probability":1,"occurences":1}},"philosophy":{"tutor,":{"probability":0.5,"occurences":2},"declined":{"probability":0.5,"occurences":2}},"topic":{"of":{"probability":1,"occurences":1}},"were":{"real":{"probability":0.04,"occurences":25},"to":{"probability":0.04,"occurences":25},"both":{"probability":0.04,"occurences":25},"on":{"probability":0.04,"occurences":25},"served;":{"probability":0.04,"occurences":25},"hysterical":{"probability":0.04,"occurences":25},"childhood":{"probability":0.04,"occurences":25},"of":{"probability":0.04,"occurences":25},"by":{"probability":0.04,"occurences":25},"substitutes":{"probability":0.04,"occurences":25},"linked,":{"probability":0.04,"occurences":25},"invited":{"probability":0.08,"occurences":50},"also":{"probability":0.04,"occurences":25},"put":{"probability":0.04,"occurences":25},"then":{"probability":0.04,"occurences":25},"elected":{"probability":0.04,"occurences":25},"consumed":{"probability":0.04,"occurences":25},"Jewish":{"probability":0.04,"occurences":25},"or":{"probability":0.04,"occurences":25},"Karl":{"probability":0.04,"occurences":25},"eventually":{"probability":0.04,"occurences":25},"implemented":{"probability":0.04,"occurences":25},"a":{"probability":0.04,"occurences":25},"taken":{"probability":0.04,"occurences":25}},"Aphasias:":{"a":{"probability":1,"occurences":1}},"president,":{"a":{"probability":1,"occurences":1}},"its":{"\"polymorphous":{"probability":0.16666666666667,"occurences":6},"existence,":{"probability":0.16666666666667,"occurences":6},"initial":{"probability":0.16666666666667,"occurences":6},"new":{"probability":0.16666666666667,"occurences":6},"growing":{"probability":0.16666666666667,"occurences":6},"first":{"probability":0.16666666666667,"occurences":6}},"Brill.":{"Important":{"probability":1,"occurences":1}},"part":{"of":{"probability":1,"occurences":1}},"Jahrbuch":{"für":{"probability":1,"occurences":1}},"controversial":{"case":{"probability":1,"occurences":1}},"tactful":{"silence":{"probability":1,"occurences":1}},"eccentric":{"theories":{"probability":1,"occurences":1}},"definite":{"ritual.":{"probability":1,"occurences":1}},"guest-book":{"entry":{"probability":1,"occurences":1}},"he":{"approach":{"probability":0.0042016806722689,"occurences":32},"middle":{"probability":0.0042016806722689,"occurences":32},"latter's":{"probability":0.0042016806722689,"occurences":32},"linked":{"probability":0.0042016806722689,"occurences":32},"treatment":{"probability":0.0042016806722689,"occurences":32},"term":{"probability":0.0042016806722689,"occurences":32},"tissue":{"probability":0.0042016806722689,"occurences":32},"psychogenetic":{"probability":0.0042016806722689,"occurences":32},"American":{"probability":0.0042016806722689,"occurences":32},"light":{"probability":0.0042016806722689,"occurences":32},"discussion":{"probability":0.0084033613445378,"occurences":64},"worldwide":{"probability":0.0042016806722689,"occurences":32},"unconscious":{"probability":0.012605042016807,"occurences":96},"work":{"probability":0.0084033613445378,"occurences":64},"retrieved":{"probability":0.0042016806722689,"occurences":32},"importance":{"probability":0.0042016806722689,"occurences":32},"last":{"probability":0.0042016806722689,"occurences":32},"Forschungen,":{"probability":0.0042016806722689,"occurences":32},"main":{"probability":0.0042016806722689,"occurences":32},"frequently":{"probability":0.0042016806722689,"occurences":32},"possible":{"probability":0.0042016806722689,"occurences":32},"nature":{"probability":0.0042016806722689,"occurences":32},"Kepler":{"probability":0.0042016806722689,"occurences":32},"Birth":{"probability":0.0042016806722689,"occurences":32},"phrase":{"probability":0.0042016806722689,"occurences":32},"Vienna":{"probability":0.0084033613445378,"occurences":64},"potential":{"probability":0.0042016806722689,"occurences":32},"event,":{"probability":0.0042016806722689,"occurences":32},"Hotel":{"probability":0.0042016806722689,"occurences":32},"neuroses.":{"probability":0.0042016806722689,"occurences":32},"granddaughter":{"probability":0.0042016806722689,"occurences":32},"Mittwochs-Gesellschaft)":{"probability":0.0042016806722689,"occurences":32},"\"drives\",":{"probability":0.0042016806722689,"occurences":32},"\"dream":{"probability":0.0042016806722689,"occurences":32},"hope":{"probability":0.0042016806722689,"occurences":32},"Salzburg":{"probability":0.0042016806722689,"occurences":32},"Freud":{"probability":0.0042016806722689,"occurences":32},"psychoanalytic":{"probability":0.012605042016807,"occurences":96},"Untimely":{"probability":0.0042016806722689,"occurences":32},"invitation":{"probability":0.0042016806722689,"occurences":32},"Burghölzli":{"probability":0.0084033613445378,"occurences":64},"said":{"probability":0.0042016806722689,"occurences":32},"Nuremberg":{"probability":0.0042016806722689,"occurences":32},"was":{"probability":0.029411764705882,"occurences":224},"Aphasias:":{"probability":0.0042016806722689,"occurences":32},"Project":{"probability":0.0042016806722689,"occurences":32},"director":{"probability":0.0084033613445378,"occurences":64},"Internationale":{"probability":0.0042016806722689,"occurences":32},"mind,":{"probability":0.0042016806722689,"occurences":32},"beginnings":{"probability":0.0042016806722689,"occurences":32},"theoretical":{"probability":0.0042016806722689,"occurences":32},"Jahrbuch":{"probability":0.0042016806722689,"occurences":32},"New":{"probability":0.0084033613445378,"occurences":64},"year":{"probability":0.0042016806722689,"occurences":32},"table":{"probability":0.0042016806722689,"occurences":32},"autumn":{"probability":0.0042016806722689,"occurences":32},"formed":{"probability":0.0042016806722689,"occurences":32},"affair.":{"probability":0.0042016806722689,"occurences":32},"heretofore":{"probability":0.0042016806722689,"occurences":32},"one":{"probability":0.0042016806722689,"occurences":32},"would":{"probability":0.0084033613445378,"occurences":64},"course":{"probability":0.0042016806722689,"occurences":32},"year,":{"probability":0.0042016806722689,"occurences":32},"Russian":{"probability":0.0042016806722689,"occurences":32},"inconsistent":{"probability":0.0042016806722689,"occurences":32},"Society":{"probability":0.0042016806722689,"occurences":32},"Unconscious":{"probability":0.0084033613445378,"occurences":64},"and":{"probability":0.0042016806722689,"occurences":32},"relinquished":{"probability":0.0042016806722689,"occurences":32},"first":{"probability":0.025210084033613,"occurences":192},"hospital.":{"probability":0.0042016806722689,"occurences":32},"discovery":{"probability":0.0042016806722689,"occurences":32},"und":{"probability":0.0042016806722689,"occurences":32},"married":{"probability":0.0042016806722689,"occurences":32},"claimed":{"probability":0.0042016806722689,"occurences":32},"Oedipus":{"probability":0.0042016806722689,"occurences":32},"concluding":{"probability":0.0042016806722689,"occurences":32},"had":{"probability":0.033613445378151,"occurences":256},"Viennese":{"probability":0.0084033613445378,"occurences":64},"most":{"probability":0.0042016806722689,"occurences":32},"post":{"probability":0.0042016806722689,"occurences":32},"impact":{"probability":0.0042016806722689,"occurences":32},"physician":{"probability":0.0042016806722689,"occurences":32},"French":{"probability":0.0042016806722689,"occurences":32},"title":{"probability":0.0042016806722689,"occurences":32},"society:":{"probability":0.0042016806722689,"occurences":32},"mid-1880s":{"probability":0.0042016806722689,"occurences":32},"residue":{"probability":0.0042016806722689,"occurences":32},"following":{"probability":0.0042016806722689,"occurences":32},"minister":{"probability":0.0042016806722689,"occurences":32},"Wednesday":{"probability":0.016806722689076,"occurences":128},"could":{"probability":0.0084033613445378,"occurences":64},"experience":{"probability":0.0042016806722689,"occurences":32},"effects":{"probability":0.0042016806722689,"occurences":32},"completion":{"probability":0.0042016806722689,"occurences":32},"plagiarism":{"probability":0.0042016806722689,"occurences":32},"university's":{"probability":0.0042016806722689,"occurences":32},"same":{"probability":0.025210084033613,"occurences":192},"theories":{"probability":0.0042016806722689,"occurences":32},"ritual":{"probability":0.0042016806722689,"occurences":32},"matter":{"probability":0.0042016806722689,"occurences":32},"field":{"probability":0.0042016806722689,"occurences":32},"etiology":{"probability":0.0042016806722689,"occurences":32},"English-speaking":{"probability":0.0042016806722689,"occurences":32},"translation":{"probability":0.0042016806722689,"occurences":32},"face-saving":{"probability":0.0042016806722689,"occurences":32},"published":{"probability":0.0084033613445378,"occurences":64},"formation":{"probability":0.0084033613445378,"occurences":64},"nose":{"probability":0.0042016806722689,"occurences":32},"clinical":{"probability":0.0084033613445378,"occurences":64},"close":{"probability":0.0042016806722689,"occurences":32},"subject":{"probability":0.0042016806722689,"occurences":32},"experienced":{"probability":0.0042016806722689,"occurences":32},"origin":{"probability":0.0042016806722689,"occurences":32},"publication":{"probability":0.0042016806722689,"occurences":32},"recognition":{"probability":0.0042016806722689,"occurences":32},"told":{"probability":0.0042016806722689,"occurences":32},"gave":{"probability":0.0042016806722689,"occurences":32},"bounding":{"probability":0.0042016806722689,"occurences":32},"suggestion":{"probability":0.0084033613445378,"occurences":64},"remedial":{"probability":0.0042016806722689,"occurences":32},"unconscious,":{"probability":0.0042016806722689,"occurences":32},"political":{"probability":0.0042016806722689,"occurences":32},"complex":{"probability":0.0042016806722689,"occurences":32},"Philosophy":{"probability":0.0042016806722689,"occurences":32},"repression":{"probability":0.0042016806722689,"occurences":32},"psychoneuroses":{"probability":0.0042016806722689,"occurences":32},"basis":{"probability":0.012605042016807,"occurences":96},"study":{"probability":0.0042016806722689,"occurences":32},"group":{"probability":0.0042016806722689,"occurences":32},"founding":{"probability":0.0042016806722689,"occurences":32},"palliative":{"probability":0.0042016806722689,"occurences":32},"Zürich":{"probability":0.0042016806722689,"occurences":32},"provided":{"probability":0.0042016806722689,"occurences":32},"tailoring":{"probability":0.0042016806722689,"occurences":32},"use":{"probability":0.0042016806722689,"occurences":32},"concepts":{"probability":0.0042016806722689,"occurences":32},"evolution":{"probability":0.0042016806722689,"occurences":32},"functioning":{"probability":0.0042016806722689,"occurences":32},"\"actual":{"probability":0.0042016806722689,"occurences":32},"Interpretation":{"probability":0.012605042016807,"occurences":96},"distinguished":{"probability":0.0042016806722689,"occurences":32},"practice":{"probability":0.0042016806722689,"occurences":32},"members":{"probability":0.0042016806722689,"occurences":32},"previous":{"probability":0.0042016806722689,"occurences":32},"death":{"probability":0.0084033613445378,"occurences":64},"intellectual":{"probability":0.0042016806722689,"occurences":32},"subsequent":{"probability":0.0042016806722689,"occurences":32},"enhanced":{"probability":0.0042016806722689,"occurences":32},"intervention":{"probability":0.0042016806722689,"occurences":32},"University":{"probability":0.012605042016807,"occurences":96},"early":{"probability":0.0084033613445378,"occurences":64},"words":{"probability":0.0042016806722689,"occurences":32},"concept.":{"probability":0.0042016806722689,"occurences":32},"psychic":{"probability":0.0042016806722689,"occurences":32},"subsequently":{"probability":0.0042016806722689,"occurences":32},"lecture":{"probability":0.0042016806722689,"occurences":32},"United":{"probability":0.0084033613445378,"occurences":64},"Psychopathology":{"probability":0.0042016806722689,"occurences":32},"regular":{"probability":0.0042016806722689,"occurences":32},"university,":{"probability":0.0042016806722689,"occurences":32},"atmosphere":{"probability":0.0042016806722689,"occurences":32},"became":{"probability":0.0042016806722689,"occurences":32},"group's":{"probability":0.0042016806722689,"occurences":32},"foundation":{"probability":0.0042016806722689,"occurences":32},"editorship":{"probability":0.0042016806722689,"occurences":32},"Nervous":{"probability":0.0042016806722689,"occurences":32},"theory":{"probability":0.012605042016807,"occurences":96},"content":{"probability":0.0042016806722689,"occurences":32},"other":{"probability":0.0042016806722689,"occurences":32},"way":{"probability":0.0042016806722689,"occurences":32},"remained":{"probability":0.0042016806722689,"occurences":32},"prevailing":{"probability":0.0042016806722689,"occurences":32},"application":{"probability":0.0042016806722689,"occurences":32},"monthly":{"probability":0.0042016806722689,"occurences":32},"Theory":{"probability":0.0084033613445378,"occurences":64},"hoped":{"probability":0.0042016806722689,"occurences":32},"called":{"probability":0.0042016806722689,"occurences":32},"Congress.":{"probability":0.0042016806722689,"occurences":32},"Congress":{"probability":0.0042016806722689,"occurences":32},"Galvanic":{"probability":0.0042016806722689,"occurences":32},"gatherings":{"probability":0.0042016806722689,"occurences":32}},"Rank.":{"Plans":{"probability":1,"occurences":1}},"analogies":{"between":{"probability":1,"occurences":1}},"Charcot,":{"a":{"probability":1,"occurences":1}},"1910":{"in":{"probability":0.25,"occurences":4},"by":{"probability":0.25,"occurences":4},"and":{"probability":0.25,"occurences":4},"where":{"probability":0.25,"occurences":4}},"lived":{"in":{"probability":1,"occurences":1}},"Society":{"the":{"probability":0.16666666666667,"occurences":6},"in":{"probability":0.33333333333333,"occurences":12},"with":{"probability":0.16666666666667,"occurences":6},"founded":{"probability":0.16666666666667,"occurences":6},"(Psychologische":{"probability":0.16666666666667,"occurences":6}},"seduction":{"theory.":{"probability":1,"occurences":1}},"interpretations":{"of":{"probability":1,"occurences":1}},"also":{"edited":{"probability":0.14285714285714,"occurences":7},"physicians":{"probability":0.14285714285714,"occurences":7},"have":{"probability":0.14285714285714,"occurences":7},"a":{"probability":0.14285714285714,"occurences":7},"influenced":{"probability":0.14285714285714,"occurences":7},"present":{"probability":0.14285714285714,"occurences":7},"drew":{"probability":0.14285714285714,"occurences":7}},"profuse,":{"recurrent":{"probability":1,"occurences":1}},"neurology.":{"Freud":{"probability":1,"occurences":1}},"Ernst":{"(b.":{"probability":1,"occurences":1}},"number":{"of":{"probability":1,"occurences":4}},"He":{"adopted":{"probability":0.14285714285714,"occurences":7},"shared":{"probability":0.14285714285714,"occurences":7},"then":{"probability":0.14285714285714,"occurences":7},"was":{"probability":0.28571428571429,"occurences":14},"believed":{"probability":0.14285714285714,"occurences":7},"died":{"probability":0.14285714285714,"occurences":7}},"journal,":{"the":{"probability":1,"occurences":1}},"chief":{"rabbi":{"probability":1,"occurences":1}},"outlook,":{"especially":{"probability":1,"occurences":1}},"travelled":{"to":{"probability":1,"occurences":1}},"Described":{"as":{"probability":1,"occurences":1}},"tasked":{"with":{"probability":1,"occurences":1}},"long-standing":{"ambition":{"probability":1,"occurences":1}},"French":{"methods":{"probability":1,"occurences":1}},"Meynert's":{"psychiatric":{"probability":1,"occurences":1}},"27":{"April":{"probability":1,"occurences":1}},"lectures":{"on":{"probability":0.66666666666667,"occurences":6},"at":{"probability":0.33333333333333,"occurences":3}},"neuroses":{"originate":{"probability":0.5,"occurences":1},"(hysteria":{"probability":0.5,"occurences":1}},"wish":{"origins":{"probability":0.16666666666667,"occurences":1},"to":{"probability":0.16666666666667,"occurences":1},"identity":{"probability":0.16666666666667,"occurences":1},"by":{"probability":0.16666666666667,"occurences":1},"friend":{"probability":0.16666666666667,"occurences":1},"mysticism\"":{"probability":0.16666666666667,"occurences":1}},"late":{"in":{"probability":1,"occurences":1}},"Hospital.":{"His":{"probability":1,"occurences":1}},"proved":{"to":{"probability":0.5,"occurences":2},"disastrous,":{"probability":0.5,"occurences":2}},"book":{"On":{"probability":0.5,"occurences":1},"entry":{"probability":0.5,"occurences":1}},"stay":{"as":{"probability":1,"occurences":1}},"Stekel":{"to":{"probability":0.5,"occurences":2},"had":{"probability":0.5,"occurences":2}},"traumatic":{"incidents":{"probability":1,"occurences":1}},"rooms":{"at":{"probability":1,"occurences":1}},"feelings":{"of":{"probability":1,"occurences":1}},"psychoanalysts":{"were":{"probability":1,"occurences":1}},"origin":{"of":{"probability":1,"occurences":1}},"recognition":{"and":{"probability":0.5,"occurences":2},"of":{"probability":0.5,"occurences":2}},"drew":{"on":{"probability":1,"occurences":4}},"1891":{"until":{"probability":1,"occurences":1}},"hypnosis":{"in":{"probability":0.33333333333333,"occurences":3},"(she":{"probability":0.33333333333333,"occurences":3},"which":{"probability":0.33333333333333,"occurences":3}},"came":{"pathogenic":{"probability":0.090909090909091,"occurences":4},"to":{"probability":0.36363636363636,"occurences":16},"the":{"probability":0.090909090909091,"occurences":4},"practicing":{"probability":0.090909090909091,"occurences":4},"one":{"probability":0.090909090909091,"occurences":4},"a":{"probability":0.18181818181818,"occurences":8},"reduced":{"probability":0.090909090909091,"occurences":4}},"origins":{"and":{"probability":0.5,"occurences":2},"of":{"probability":0.5,"occurences":2}},"Relation":{"to":{"probability":1,"occurences":1}},"significant":{"breakthrough":{"probability":0.5,"occurences":2},"influence":{"probability":0.5,"occurences":2}},"1899":{"he":{"probability":1,"occurences":1}},"catalytic":{"in":{"probability":1,"occurences":1}},"use":{"of":{"probability":0.5,"occurences":9},"suggestion.":{"probability":0.16666666666667,"occurences":3},"in":{"probability":0.33333333333333,"occurences":6}},"school.":{"Prior":{"probability":1,"occurences":1}},"Sexuality":{"in":{"probability":1,"occurences":1}},"1896":{"he":{"probability":0.5,"occurences":2},"and":{"probability":0.5,"occurences":2}},"practice":{"of":{"probability":0.25,"occurences":4},"specializing":{"probability":0.25,"occurences":4},"back":{"probability":0.25,"occurences":4},"psychoanalysis":{"probability":0.25,"occurences":4}},"intervention":{"of":{"probability":1,"occurences":1}},"institutional":{"status,":{"probability":1,"occurences":1}},"collusion":{"in":{"probability":1,"occurences":1}},"Congress,":{"was":{"probability":1,"occurences":1}},"matter":{"whether":{"probability":0.5,"occurences":2},"or":{"probability":0.5,"occurences":2}},"disorders\".":{"The":{"probability":1,"occurences":1}},"regular":{"nasal":{"probability":0.5,"occurences":1},"series":{"probability":0.5,"occurences":1}},"Their":{"friendship":{"probability":1,"occurences":1}},"April":{"1908.":{"probability":1,"occurences":1}},"Reitler":{"was":{"probability":0.33333333333333,"occurences":3},"were":{"probability":0.33333333333333,"occurences":3},"went":{"probability":0.33333333333333,"occurences":3}},"black":{"coffee":{"probability":1,"occurences":1}},"superficial.":{"By":{"probability":1,"occurences":1}},"Berggasse":{"19,":{"probability":0.5,"occurences":2},"19":{"probability":0.5,"occurences":2}},"sexuality":{"–":{"probability":0.25,"occurences":3},"and":{"probability":0.25,"occurences":3},"to":{"probability":0.25,"occurences":3},"provided":{"probability":0.25,"occurences":3}},"restored":{"to":{"probability":1,"occurences":1}},"Theory":{"of":{"probability":1,"occurences":4}},"York-based":{"Abraham":{"probability":1,"occurences":1}},"sinuses":{"to":{"probability":1,"occurences":1}},"hall":{"of":{"probability":1,"occurences":1}},"thus":{"enhanced,":{"probability":1,"occurences":1}},"on":{"and":{"probability":0.056818181818182,"occurences":125},"which,":{"probability":0.022727272727273,"occurences":50},"in":{"probability":0.045454545454545,"occurences":100},"has":{"probability":0.011363636363636,"occurences":25},"the":{"probability":0.10227272727273,"occurences":225},"group":{"probability":0.011363636363636,"occurences":25},"after":{"probability":0.011363636363636,"occurences":25},"stage":{"probability":0.011363636363636,"occurences":25},"aphasia":{"probability":0.011363636363636,"occurences":25},"Krafft-Ebing.":{"probability":0.011363636363636,"occurences":25},"over":{"probability":0.011363636363636,"occurences":25},"now":{"probability":0.011363636363636,"occurences":25},"Hartmann's":{"probability":0.011363636363636,"occurences":25},"group.":{"probability":0.011363636363636,"occurences":25},"rights":{"probability":0.011363636363636,"occurences":25},"infantile":{"probability":0.011363636363636,"occurences":25},"to":{"probability":0.090909090909091,"occurences":200},"he":{"probability":0.011363636363636,"occurences":25},"appear":{"probability":0.011363636363636,"occurences":25},"of":{"probability":0.27272727272727,"occurences":600},"theory.":{"probability":0.011363636363636,"occurences":25},"Hysteria":{"probability":0.011363636363636,"occurences":25},"for":{"probability":0.011363636363636,"occurences":25},"would":{"probability":0.011363636363636,"occurences":25},"concerns":{"probability":0.011363636363636,"occurences":25},"a":{"probability":0.022727272727273,"occurences":50},"Freud's":{"probability":0.011363636363636,"occurences":25},"hypnosis,":{"probability":0.011363636363636,"occurences":25},"Fliess's":{"probability":0.011363636363636,"occurences":25},"from":{"probability":0.022727272727273,"occurences":50},"psychoanalysis.":{"probability":0.011363636363636,"occurences":25},"27":{"probability":0.011363636363636,"occurences":25},"which":{"probability":0.034090909090909,"occurences":75},"Putnam,":{"probability":0.011363636363636,"occurences":25},"with":{"probability":0.022727272727273,"occurences":50},"his":{"probability":0.022727272727273,"occurences":50}},"edited":{"by":{"probability":1,"occurences":9}},"later":{"in":{"probability":0.33333333333333,"occurences":3},"concluded":{"probability":0.33333333333333,"occurences":3},"to":{"probability":0.33333333333333,"occurences":3}},"can":{"be":{"probability":0.33333333333333,"occurences":1},"Psychoanalytic":{"probability":0.33333333333333,"occurences":1},"academic":{"probability":0.33333333333333,"occurences":1}},"homoerotic":{"attachment":{"probability":1,"occurences":1}},"\"wish-bleedings\"":{"linked":{"probability":1,"occurences":1}},"severe":{"leg":{"probability":1,"occurences":1}},"treatment":{"of":{"probability":0.5,"occurences":2},"by":{"probability":0.5,"occurences":2}},"herself.":{"Freud,":{"probability":1,"occurences":1}},"interest":{"in":{"probability":1,"occurences":9}},"realised":{"his":{"probability":1,"occurences":1}},"discovered":{"Freud's":{"probability":1,"occurences":1}},"full":{"mobility":{"probability":1,"occurences":1}},"found":{"that":{"probability":1,"occurences":1}},"physically":{"manifested":{"probability":1,"occurences":1}},"stomach":{"and":{"probability":1,"occurences":1}},"Diseases":{"of":{"probability":1,"occurences":1}},"half":{"of":{"probability":1,"occurences":1}},"On":{"the":{"probability":0.66666666666667,"occurences":6},"Dreams,":{"probability":0.33333333333333,"occurences":3}},"Jokes":{"and":{"probability":1,"occurences":1}},"support,":{"as":{"probability":1,"occurences":1}},"metapsychology":{"with":{"probability":1,"occurences":1}},"main":{"contemporary":{"probability":1,"occurences":1}},"case":{"studies.":{"probability":0.33333333333333,"occurences":3},"they":{"probability":0.33333333333333,"occurences":3},"histories,":{"probability":0.33333333333333,"occurences":3}},"correspondence":{"with":{"probability":0.4,"occurences":10},"from":{"probability":0.2,"occurences":5},"to":{"probability":0.2,"occurences":5},"between":{"probability":0.2,"occurences":5}},"small":{"audiences":{"probability":0.5,"occurences":2},"psychoanalytic":{"probability":0.5,"occurences":2}},"anatomy":{"led":{"probability":1,"occurences":1}},"empathy.":{"Though":{"probability":1,"occurences":1}},"publication":{"of":{"probability":1,"occurences":4}},"pathogenic":{"only":{"probability":1,"occurences":1}},"type":{"of":{"probability":1,"occurences":1}},"Margarete":{"Hilferding,":{"probability":1,"occurences":1}},"Wednesday":{"Psychological":{"probability":0.4,"occurences":10},"afternoon":{"probability":0.2,"occurences":5},"group":{"probability":0.4,"occurences":10}},"October":{"1885,":{"probability":1,"occurences":1}},"tailoring":{"trade.":{"probability":1,"occurences":1}},"joined":{"the":{"probability":0.66666666666667,"occurences":6},"by":{"probability":0.33333333333333,"occurences":3}},"problem":{"or":{"probability":1,"occurences":1}},"demonstrate":{"the":{"probability":1,"occurences":1}},"cerebral":{"anatomy":{"probability":1,"occurences":1}},"Freud":{"also":{"probability":0.019230769230769,"occurences":52},"read":{"probability":0.038461538461538,"occurences":104},"founded":{"probability":0.019230769230769,"occurences":52},"himself":{"probability":0.019230769230769,"occurences":52},"household":{"probability":0.019230769230769,"occurences":52},"whilst":{"probability":0.019230769230769,"occurences":52},"were":{"probability":0.019230769230769,"occurences":52},"abandoned":{"probability":0.019230769230769,"occurences":52},"invited":{"probability":0.019230769230769,"occurences":52},"was":{"probability":0.038461538461538,"occurences":104},"fled":{"probability":0.019230769230769,"occurences":52},"owned":{"probability":0.019230769230769,"occurences":52},"found":{"probability":0.019230769230769,"occurences":52},"began":{"probability":0.076923076923077,"occurences":208},"ultimately,":{"probability":0.019230769230769,"occurences":52},"elaborates":{"probability":0.019230769230769,"occurences":52},"to":{"probability":0.038461538461538,"occurences":104},"bought":{"probability":0.019230769230769,"occurences":52},"turned":{"probability":0.019230769230769,"occurences":52},"suggested":{"probability":0.019230769230769,"occurences":52},"valued":{"probability":0.019230769230769,"occurences":52},"applied":{"probability":0.019230769230769,"occurences":52},"as":{"probability":0.019230769230769,"occurences":52},"circle,":{"probability":0.019230769230769,"occurences":52},"for":{"probability":0.038461538461538,"occurences":104},"and":{"probability":0.096153846153846,"occurences":260},"described":{"probability":0.019230769230769,"occurences":52},"led":{"probability":0.019230769230769,"occurences":52},"worked":{"probability":0.019230769230769,"occurences":52},"had":{"probability":0.057692307692308,"occurences":156},"resigned":{"probability":0.019230769230769,"occurences":52},"at":{"probability":0.019230769230769,"occurences":52},"went":{"probability":0.019230769230769,"occurences":52},"himself.":{"probability":0.019230769230769,"occurences":52},"gives":{"probability":0.019230769230769,"occurences":52},"came":{"probability":0.019230769230769,"occurences":52},"continued":{"probability":0.019230769230769,"occurences":52},"in":{"probability":0.019230769230769,"occurences":52},"drew":{"probability":0.019230769230769,"occurences":52}},"invitation":{"of":{"probability":1,"occurences":1}},"1884":{"and":{"probability":1,"occurences":1}},"medicine":{"at":{"probability":1,"occurences":1}},"further":{"his":{"probability":1,"occurences":1}},"two":{"of":{"probability":0.5,"occurences":1},"present,":{"probability":0.5,"occurences":1}},"professor.":{"The":{"probability":1,"occurences":1}},"formulation":{"of":{"probability":0.5,"occurences":2},"now":{"probability":0.5,"occurences":2}},"financially":{"promising":{"probability":1,"occurences":1}},"afternoon":{"to":{"probability":1,"occurences":1}},"1907":{"for":{"probability":1,"occurences":1}},"psychiatric":{"clinic.":{"probability":0.5,"occurences":2},"clinic":{"probability":0.5,"occurences":2}},"General":{"Hospital.":{"probability":1,"occurences":1}},"(1905).":{"In":{"probability":1,"occurences":1}},"(the":{"unconscious,":{"probability":1,"occurences":1}},"six":{"children:":{"probability":1,"occurences":1}},"spoken":{"by":{"probability":1,"occurences":1}},"consequent":{"restricted":{"probability":0.5,"occurences":2},"over-estimation":{"probability":0.5,"occurences":2}},"symptoms":{"became":{"probability":0.33333333333333,"occurences":3},"included":{"probability":0.33333333333333,"occurences":3},"while":{"probability":0.33333333333333,"occurences":3}},"rely":{"on":{"probability":0.5,"occurences":1},"in":{"probability":0.5,"occurences":1}},"Essays":{"on":{"probability":1,"occurences":4}},"Russian":{"psychiatrists":{"probability":0.5,"occurences":2},"Psychoanalytic":{"probability":0.5,"occurences":2}},"formation.":{"By":{"probability":1,"occurences":1}},"post-operative":{"haemorrhages":{"probability":1,"occurences":1}},"theory":{"of":{"probability":0.875,"occurences":56},"that":{"probability":0.125,"occurences":8}},"died":{"medicine":{"probability":0.5,"occurences":1},"prematurely":{"probability":0.5,"occurences":1}},"\"as":{"texts":{"probability":1,"occurences":1}},"reflex":{"neurosis\",":{"probability":1,"occurences":1}},"describing":{"its":{"probability":1,"occurences":1}},"highly":{"eccentric":{"probability":1,"occurences":1}},"basis":{"of":{"probability":0.66666666666667,"occurences":6},"for":{"probability":0.33333333333333,"occurences":3}},"University,":{"he":{"probability":0.33333333333333,"occurences":3},"although":{"probability":0.33333333333333,"occurences":3},"Worcester,":{"probability":0.33333333333333,"occurences":3}},"recurrent":{"nasal":{"probability":1,"occurences":1}},"academically":{"acclaimed":{"probability":1,"occurences":1}},"claimed":{"researcher":{"probability":0.5,"occurences":1},"not":{"probability":0.5,"occurences":1}},"functioning":{"of":{"probability":1,"occurences":1}},"Ferenczi,":{"in":{"probability":1,"occurences":1}},"Krafft-Ebing.":{"His":{"probability":1,"occurences":1}},"post":{"but":{"probability":0.25,"occurences":4},"(he":{"probability":0.25,"occurences":4},"and":{"probability":0.25,"occurences":4},"at":{"probability":0.25,"occurences":4}},"life.":{"One":{"probability":0.5,"occurences":2},"Jones's":{"probability":0.5,"occurences":2}},"Herbart":{"with":{"probability":1,"occurences":1}},"neurosis),":{"a":{"probability":1,"occurences":1}},"interpretation":{"and":{"probability":1,"occurences":1}},"terms":{"of":{"probability":1,"occurences":1}},"returned":{"to":{"probability":1,"occurences":1}},"opened":{"them.":{"probability":0.5,"occurences":2},"an":{"probability":0.5,"occurences":2}},"1886,":{"Freud":{"probability":1,"occurences":4}},"valued":{"and":{"probability":1,"occurences":1}},"Freud's":{"seduction":{"probability":0.037037037037037,"occurences":27},"early":{"probability":0.074074074074074,"occurences":54},"work.":{"probability":0.037037037037037,"occurences":27},"work":{"probability":0.14814814814815,"occurences":108},"sister,":{"probability":0.037037037037037,"occurences":27},"Jewish":{"probability":0.037037037037037,"occurences":27},"support,":{"probability":0.037037037037037,"occurences":27},"psychoanalytic":{"probability":0.037037037037037,"occurences":27},"offer":{"probability":0.037037037037037,"occurences":27},"efforts":{"probability":0.037037037037037,"occurences":27},"work,":{"probability":0.037037037037037,"occurences":27},"consulting":{"probability":0.037037037037037,"occurences":27},"subsequent":{"probability":0.037037037037037,"occurences":27},"works,":{"probability":0.037037037037037,"occurences":27},"views":{"probability":0.037037037037037,"occurences":27},"audience":{"probability":0.037037037037037,"occurences":27},"clinical":{"probability":0.037037037037037,"occurences":27},"developing":{"probability":0.037037037037037,"occurences":27},"development":{"probability":0.037037037037037,"occurences":27},"writings":{"probability":0.037037037037037,"occurences":27},"unwillingness":{"probability":0.037037037037037,"occurences":27},"correspondence":{"probability":0.037037037037037,"occurences":27},"visit":{"probability":0.037037037037037,"occurences":27}},"1938,":{"Freud":{"probability":1,"occurences":1}},"Approach":{"to":{"probability":1,"occurences":1}},"face-saving":{"topic":{"probability":1,"occurences":1}},"published":{"in":{"probability":0.57142857142857,"occurences":28},"The":{"probability":0.14285714285714,"occurences":7},"Fragment":{"probability":0.14285714285714,"occurences":7},"research":{"probability":0.14285714285714,"occurences":7}},"24;":{"initially":{"probability":1,"occurences":1}},"ideas.":{"His":{"probability":1,"occurences":1}},"father":{"in":{"probability":0.33333333333333,"occurences":3},"and":{"probability":0.33333333333333,"occurences":3},"of":{"probability":0.33333333333333,"occurences":3}},"Schopenhauer":{"and":{"probability":1,"occurences":1}},"insights":{"with":{"probability":1,"occurences":1}},"Tagblatt.":{"The":{"probability":1,"occurences":1}},"Other":{"texts":{"probability":1,"occurences":1}},"derived":{"from":{"probability":1,"occurences":1}},"real":{"or":{"probability":1,"occurences":1}},"encouraging":{"patients":{"probability":1,"occurences":1}},"Interpretation":{"of":{"probability":1,"occurences":9}},"behind":{"his":{"probability":1,"occurences":1}},"causative":{"function,":{"probability":1,"occurences":1}},"gauze":{"in":{"probability":1,"occurences":1}},"concept.":{"Freud":{"probability":1,"occurences":1}},"1906,":{"the":{"probability":0.5,"occurences":2},"their":{"probability":0.5,"occurences":2}},"permanent":{"member":{"probability":1,"occurences":1}},"declined":{"after":{"probability":1,"occurences":1}},"Eckstein's":{"post-operative":{"probability":0.25,"occurences":4},"hysteria.":{"probability":0.25,"occurences":4},"nasal":{"probability":0.25,"occurences":4},"history":{"probability":0.25,"occurences":4}},"end.":{"In":{"probability":1,"occurences":1}},"colleague":{"Wilhelm":{"probability":1,"occurences":1}},"discussions":{"over":{"probability":1,"occurences":1}},"lay":{"symptom":{"probability":0.5,"occurences":1},"behind":{"probability":0.5,"occurences":1}},"his":{"work.":{"probability":0.011904761904762,"occurences":73},"Three":{"probability":0.011904761904762,"occurences":73},"Jewish":{"probability":0.011904761904762,"occurences":73},"first":{"probability":0.011904761904762,"occurences":73},"transition":{"probability":0.011904761904762,"occurences":73},"adolescent":{"probability":0.011904761904762,"occurences":73},"disastrous":{"probability":0.011904761904762,"occurences":73},"secular":{"probability":0.011904761904762,"occurences":73},"reading":{"probability":0.011904761904762,"occurences":73},"discussion":{"probability":0.023809523809524,"occurences":146},"Psychology":{"probability":0.011904761904762,"occurences":73},"family":{"probability":0.011904761904762,"occurences":73},"work":{"probability":0.035714285714286,"occurences":219},"Autobiographical":{"probability":0.011904761904762,"occurences":73},"own":{"probability":0.035714285714286,"occurences":219},"sister-in-law,":{"probability":0.011904761904762,"occurences":73},"respect.":{"probability":0.011904761904762,"occurences":73},"friend":{"probability":0.023809523809524,"occurences":146},"theories":{"probability":0.023809523809524,"occurences":146},"friend,":{"probability":0.011904761904762,"occurences":73},"medical":{"probability":0.023809523809524,"occurences":146},"stay":{"probability":0.011904761904762,"occurences":73},"nose":{"probability":0.011904761904762,"occurences":73},"father":{"probability":0.023809523809524,"occurences":146},"feelings":{"probability":0.011904761904762,"occurences":73},"procedure,":{"probability":0.011904761904762,"occurences":73},"new":{"probability":0.011904761904762,"occurences":73},"loyalty":{"probability":0.011904761904762,"occurences":73},"formative":{"probability":0.011904761904762,"occurences":73},"psychoanalytic":{"probability":0.011904761904762,"occurences":73},"group":{"probability":0.011904761904762,"occurences":73},"allegiance":{"probability":0.011904761904762,"occurences":73},"philosophy":{"probability":0.011904761904762,"occurences":73},"was":{"probability":0.011904761904762,"occurences":73},"Project":{"probability":0.011904761904762,"occurences":73},"patient":{"probability":0.011904761904762,"occurences":73},"meeting,":{"probability":0.011904761904762,"occurences":73},"intellectual":{"probability":0.023809523809524,"occurences":146},"theoretical":{"probability":0.011904761904762,"occurences":73},"hospital":{"probability":0.011904761904762,"occurences":73},"early":{"probability":0.011904761904762,"occurences":73},"mother's":{"probability":0.011904761904762,"occurences":73},"account":{"probability":0.011904761904762,"occurences":73},"general":{"probability":0.011904761904762,"occurences":73},"prestige":{"probability":0.011904761904762,"occurences":73},"more":{"probability":0.023809523809524,"occurences":146},"way,":{"probability":0.011904761904762,"occurences":73},"collected":{"probability":0.011904761904762,"occurences":73},"consequent":{"probability":0.011904761904762,"occurences":73},"work,":{"probability":0.011904761904762,"occurences":73},"patients'":{"probability":0.011904761904762,"occurences":73},"Saturday":{"probability":0.011904761904762,"occurences":73},"appointment":{"probability":0.023809523809524,"occurences":146},"understanding":{"probability":0.011904761904762,"occurences":73},"theory":{"probability":0.035714285714286,"occurences":219},"successful":{"probability":0.011904761904762,"occurences":73},"country":{"probability":0.011904761904762,"occurences":73},"capacity":{"probability":0.011904761904762,"occurences":73},"clinical":{"probability":0.035714285714286,"occurences":219},"\"specifically":{"probability":0.011904761904762,"occurences":73},"correspondence":{"probability":0.011904761904762,"occurences":73},"life,":{"probability":0.011904761904762,"occurences":73},"letters":{"probability":0.011904761904762,"occurences":73},"ambition":{"probability":0.011904761904762,"occurences":73},"apartment":{"probability":0.011904761904762,"occurences":73},"long-standing":{"probability":0.011904761904762,"occurences":73},"increasingly":{"probability":0.011904761904762,"occurences":73},"self-analysis,":{"probability":0.011904761904762,"occurences":73}},"inception,":{"described":{"probability":1,"occurences":1}},"explanation":{"of":{"probability":1,"occurences":1}},"sexuality.":{"Fliess":{"probability":1,"occurences":1}},"university,":{"his":{"probability":1,"occurences":1}},"decisive":{"word":{"probability":1,"occurences":1}},"out-patient":{"psychotherapy":{"probability":1,"occurences":1}},"paper":{"on":{"probability":0.5,"occurences":1},"Neues":{"probability":0.5,"occurences":1}},"effect":{"on":{"probability":1,"occurences":1}},"evidence":{"of":{"probability":1,"occurences":1}},"Sophie":{"(b.":{"probability":1,"occurences":1}},"university's":{"psychiatric":{"probability":1,"occurences":1}},"himself.":{"There":{"probability":1,"occurences":1}},"smoker,":{"he":{"probability":0.5,"occurences":2},"eventually":{"probability":0.5,"occurences":2}},"begun":{"applying":{"probability":1,"occurences":1}},"His":{"interest":{"probability":0.125,"occurences":8},"substantial":{"probability":0.125,"occurences":8},"conversion":{"probability":0.125,"occurences":8},"explorations":{"probability":0.125,"occurences":8},"time":{"probability":0.125,"occurences":8},"English":{"probability":0.125,"occurences":8},"research":{"probability":0.125,"occurences":8},"first":{"probability":0.125,"occurences":8}},"Standpoint":{"(1874).":{"probability":1,"occurences":1}},"histories,":{"in":{"probability":1,"occurences":1}},"approach":{"of":{"probability":1,"occurences":1}},"started":{"by":{"probability":1,"occurences":1}},"refer":{"to":{"probability":1,"occurences":1}},"since":{"the":{"probability":1,"occurences":1}},"hysteria":{"and":{"probability":1,"occurences":2}},"conscious)":{"on":{"probability":1,"occurences":1}},"fundamentally":{"revise":{"probability":1,"occurences":1}},"Lipps":{"who":{"probability":1,"occurences":1}},"pains.":{"These":{"probability":1,"occurences":1}},"up":{"soon":{"probability":0.1,"occurences":4},"again":{"probability":0.2,"occurences":8},"was":{"probability":0.2,"occurences":8},"in":{"probability":0.2,"occurences":8},"had":{"probability":0.1,"occurences":4},"a":{"probability":0.1,"occurences":4},"at":{"probability":0.1,"occurences":4}},"Jean-Martin":{"(b.":{"probability":0.5,"occurences":2},"Charcot,":{"probability":0.5,"occurences":2}},"front":{"of":{"probability":1,"occurences":1}},"severity":{"as":{"probability":1,"occurences":1}},"elaborates":{"his":{"probability":1,"occurences":1}},"contributions":{"to":{"probability":1,"occurences":1}},"personal":{"details":{"probability":1,"occurences":1}},"notable":{"for":{"probability":1,"occurences":1}},"nature":{"of":{"probability":1,"occurences":1}},"Kepler":{"of":{"probability":1,"occurences":1}},"tobacco,":{"were":{"probability":1,"occurences":1}},"audiences":{"every":{"probability":1,"occurences":1}},"views":{"on":{"probability":1,"occurences":1}},"complex.":{"Freud":{"probability":1,"occurences":1}},"primarily":{"neurasthenia":{"probability":1,"occurences":1}},"existence":{"of":{"probability":1,"occurences":1}},"abridged":{"version,":{"probability":1,"occurences":1}},"(1874).":{"Although":{"probability":1,"occurences":1}},"a":{"and":{"probability":0.03125,"occurences":216},"Spielrein":{"probability":0.010416666666667,"occurences":72},"in":{"probability":0.020833333333333,"occurences":144},"Case":{"probability":0.010416666666667,"occurences":72},"\"neurasthenia\"":{"probability":0.010416666666667,"occurences":72},"Baroness":{"probability":0.010416666666667,"occurences":72},"University,":{"probability":0.010416666666667,"occurences":72},"buccal":{"probability":0.010416666666667,"occurences":72},"three-year":{"probability":0.010416666666667,"occurences":72},"platform":{"probability":0.010416666666667,"occurences":72},"chief":{"probability":0.010416666666667,"occurences":72},"Viennese":{"probability":0.010416666666667,"occurences":72},"post":{"probability":0.010416666666667,"occurences":72},"journal":{"probability":0.010416666666667,"occurences":72},"nasogenital":{"probability":0.010416666666667,"occurences":72},"Scientific":{"probability":0.010416666666667,"occurences":72},"to":{"probability":0.010416666666667,"occurences":72},"docent":{"probability":0.010416666666667,"occurences":72},"social":{"probability":0.010416666666667,"occurences":72},"metapsychology":{"probability":0.010416666666667,"occurences":72},"valuable":{"probability":0.010416666666667,"occurences":72},"positive":{"probability":0.010416666666667,"occurences":72},"correspondence":{"probability":0.010416666666667,"occurences":72},"small":{"probability":0.010416666666667,"occurences":72},"published":{"probability":0.010416666666667,"occurences":72},"lecturer":{"probability":0.010416666666667,"occurences":72},"career":{"probability":0.010416666666667,"occurences":72},"type":{"probability":0.010416666666667,"occurences":72},"university":{"probability":0.020833333333333,"occurences":144},"period":{"probability":0.020833333333333,"occurences":144},"health":{"probability":0.010416666666667,"occurences":72},"causative":{"probability":0.010416666666667,"occurences":72},"necessary":{"probability":0.010416666666667,"occurences":72},"cigar":{"probability":0.010416666666667,"occurences":72},"significant":{"probability":0.010416666666667,"occurences":72},"sexual":{"probability":0.010416666666667,"occurences":72},"more":{"probability":0.010416666666667,"occurences":72},"homoerotic":{"probability":0.010416666666667,"occurences":72},"combination":{"probability":0.010416666666667,"occurences":72},"renowned":{"probability":0.010416666666667,"occurences":72},"Eckstein":{"probability":0.010416666666667,"occurences":72},"Swiss":{"probability":0.020833333333333,"occurences":144},"permanent":{"probability":0.010416666666667,"occurences":72},"half-metre":{"probability":0.010416666666667,"occurences":72},"socialist":{"probability":0.010416666666667,"occurences":72},"were":{"probability":0.010416666666667,"occurences":72},"critical":{"probability":0.010416666666667,"occurences":72},"Critical":{"probability":0.010416666666667,"occurences":72},"Rosenthal":{"probability":0.010416666666667,"occurences":72},"formulation":{"probability":0.010416666666667,"occurences":72},"general":{"probability":0.010416666666667,"occurences":72},"result":{"probability":0.010416666666667,"occurences":72},"three-month":{"probability":0.010416666666667,"occurences":72},"conference":{"probability":0.010416666666667,"occurences":72},"cigarette":{"probability":0.010416666666667,"occurences":72},"under":{"probability":0.010416666666667,"occurences":72},"Freud's":{"probability":0.010416666666667,"occurences":72},"non-salaried":{"probability":0.010416666666667,"occurences":72},"number":{"probability":0.020833333333333,"occurences":144},"(b.":{"probability":0.010416666666667,"occurences":72},"General":{"probability":0.010416666666667,"occurences":72},"means":{"probability":0.010416666666667,"occurences":72},"tactful":{"probability":0.010416666666667,"occurences":72},"definite":{"probability":0.010416666666667,"occurences":72},"religion":{"probability":0.010416666666667,"occurences":72},"local":{"probability":0.010416666666667,"occurences":72},"Psychoanalytic":{"probability":0.010416666666667,"occurences":72},"historical":{"probability":0.010416666666667,"occurences":72},"less":{"probability":0.010416666666667,"occurences":72},"Bernays,":{"probability":0.020833333333333,"occurences":144},"London-based":{"probability":0.010416666666667,"occurences":72},"paper.":{"probability":0.010416666666667,"occurences":72},"position":{"probability":0.010416666666667,"occurences":72},"following":{"probability":0.010416666666667,"occurences":72},"view":{"probability":0.010416666666667,"occurences":72},"would":{"probability":0.010416666666667,"occurences":72},"Berlin-based":{"probability":0.010416666666667,"occurences":72},"locum":{"probability":0.010416666666667,"occurences":72},"bridge":{"probability":0.010416666666667,"occurences":72},"division":{"probability":0.010416666666667,"occurences":72},"as":{"probability":0.010416666666667,"occurences":72},"substantial":{"probability":0.010416666666667,"occurences":72},"interest.":{"probability":0.010416666666667,"occurences":72},"smoker,":{"probability":0.010416666666667,"occurences":72},"\"self-analysis\"":{"probability":0.010416666666667,"occurences":72},"patient":{"probability":0.010416666666667,"occurences":72},"O.,":{"probability":0.010416666666667,"occurences":72},"systematic":{"probability":0.010416666666667,"occurences":72}},"Emma":{"Eckstein":{"probability":1,"occurences":1}},"neuropathology.":{"This":{"probability":1,"occurences":1}},"O.,":{"she":{"probability":1,"occurences":1}},"psychoanalysis":{"herself.":{"probability":0.33333333333333,"occurences":3},"is":{"probability":0.33333333333333,"occurences":3},"to":{"probability":0.33333333333333,"occurences":3}},"theorists":{"of":{"probability":1,"occurences":1}},"Innere":{"Stadt,":{"probability":1,"occurences":1}},"Jung":{"was":{"probability":0.16666666666667,"occurences":6},"who":{"probability":0.16666666666667,"occurences":6},"and":{"probability":0.33333333333333,"occurences":12},"at":{"probability":0.33333333333333,"occurences":12}},"room.":{"Freud":{"probability":1,"occurences":1}},"introduced":{"Stekel":{"probability":1,"occurences":1}},"Project":{"for":{"probability":0.5,"occurences":2},"were":{"probability":0.5,"occurences":2}},"Eitingon":{"from":{"probability":1,"occurences":1}},"mental":{"structure":{"probability":1,"occurences":1}},"Charcot":{"specialized":{"probability":1,"occurences":1}},"Hartmann's":{"The":{"probability":1,"occurences":1}},"cigarettes":{"were":{"probability":1,"occurences":1}},"year":{"and":{"probability":0.16666666666667,"occurences":5},"she":{"probability":0.16666666666667,"occurences":5},"of":{"probability":0.16666666666667,"occurences":5},"he":{"probability":0.33333333333333,"occurences":10},"period,":{"probability":0.16666666666667,"occurences":5}},"quantities.":{"After":{"probability":1,"occurences":1}},"aware":{"of":{"probability":1,"occurences":1}},"clinic":{"and":{"probability":1,"occurences":1}},"there":{"being":{"probability":1,"occurences":1}},"over-estimation":{"of":{"probability":1,"occurences":1}},"understanding":{"of":{"probability":1,"occurences":1}},"bisexuality":{"to":{"probability":1,"occurences":1}},"Berlin-based":{"ear,":{"probability":1,"occurences":1}},"bridge":{"between":{"probability":1,"occurences":1}},"occurred":{"to":{"probability":1,"occurences":1}},"represented":{"a":{"probability":1,"occurences":1}},"philosophical":{"theories,":{"probability":1,"occurences":1}},"Martha":{"Bernays,":{"probability":0.5,"occurences":2},"Freud's":{"probability":0.5,"occurences":2}},"half-metre":{"of":{"probability":1,"occurences":1}},"Jewish":{"origins":{"probability":0.2,"occurences":5},"friend":{"probability":0.2,"occurences":5},"mysticism\"":{"probability":0.2,"occurences":5},"identity":{"probability":0.2,"occurences":5},"by":{"probability":0.2,"occurences":5}},"first":{"introduced":{"probability":0.083333333333333,"occurences":12},"to":{"probability":0.083333333333333,"occurences":12},"International":{"probability":0.083333333333333,"occurences":12},"woman":{"probability":0.083333333333333,"occurences":12},"two":{"probability":0.083333333333333,"occurences":12},"met":{"probability":0.083333333333333,"occurences":12},"book":{"probability":0.083333333333333,"occurences":12},"attempt":{"probability":0.083333333333333,"occurences":12},"president.":{"probability":0.083333333333333,"occurences":12},"time":{"probability":0.083333333333333,"occurences":12},"public":{"probability":0.083333333333333,"occurences":12},"encountered":{"probability":0.083333333333333,"occurences":12}},"then":{"already":{"probability":0.25,"occurences":4},"called":{"probability":0.25,"occurences":4},"a":{"probability":0.25,"occurences":4},"sets":{"probability":0.25,"occurences":4}},"discovery":{"of":{"probability":1,"occurences":1}},"Psychological":{"Society":{"probability":1,"occurences":4}},"old":{"after":{"probability":0.33333333333333,"occurences":1},"his":{"probability":0.33333333333333,"occurences":1},"wish":{"probability":0.33333333333333,"occurences":1}},"study":{"with":{"probability":0.5,"occurences":2},"of":{"probability":0.5,"occurences":2}},"whether":{"they":{"probability":1,"occurences":1}},"Oedipus":{"complex.":{"probability":1,"occurences":1}},"international":{"association":{"probability":1,"occurences":1}},"Viennese":{"daily":{"probability":0.25,"occurences":4},"musicologist":{"probability":0.25,"occurences":4},"and":{"probability":0.25,"occurences":4},"physicians":{"probability":0.25,"occurences":4}},"developing":{"ideas":{"probability":1,"occurences":1}},"discussed":{"the":{"probability":1,"occurences":1}},"referred":{"his":{"probability":1,"occurences":1}},"trade.":{"He":{"probability":1,"occurences":1}},"present":{"a":{"probability":0.5,"occurences":2},"and":{"probability":0.5,"occurences":2}},"attend":{"the":{"probability":1,"occurences":1}},"Prior":{"to":{"probability":1,"occurences":1}},"theories":{"of":{"probability":0.5,"occurences":18},"took":{"probability":0.16666666666667,"occurences":6},"on":{"probability":0.16666666666667,"occurences":6},"outside":{"probability":0.16666666666667,"occurences":6}},"International":{"Psychoanalytic":{"probability":1,"occurences":1}},"common":{"concerns":{"probability":1,"occurences":1}},"called":{"the":{"probability":0.5,"occurences":8},"Fliess":{"probability":0.25,"occurences":4},"\"free":{"probability":0.25,"occurences":4}},"repressed":{"memories.":{"probability":1,"occurences":1}},"entitled":{"him":{"probability":1,"occurences":1}},"gave":{"a":{"probability":0.5,"occurences":2},"five":{"probability":0.5,"occurences":2}},"fruitfully":{"analyzed":{"probability":1,"occurences":1}},"she":{"Forschungen,":{"probability":0.16666666666667,"occurences":4},"formed":{"probability":0.16666666666667,"occurences":4},"would":{"probability":0.16666666666667,"occurences":4},"was":{"probability":0.33333333333333,"occurences":8},"retrieved":{"probability":0.16666666666667,"occurences":4}},"broke":{"with":{"probability":1,"occurences":1}},"both":{"Schopenhauer":{"probability":0.2,"occurences":5},"his":{"probability":0.2,"occurences":5},"he":{"probability":0.2,"occurences":5},"of":{"probability":0.2,"occurences":5},"Russian":{"probability":0.2,"occurences":5}},"patients'":{"dreams":{"probability":1,"occurences":4}},"recall":{"the":{"probability":1,"occurences":1}},"famous":{"and":{"probability":1,"occurences":1}},"\"specifically":{"Jewish":{"probability":1,"occurences":1}},"efforts":{"to":{"probability":1,"occurences":1}},"methods":{"of":{"probability":0.33333333333333,"occurences":3},"he":{"probability":0.33333333333333,"occurences":3},"in":{"probability":0.33333333333333,"occurences":3}},"1910.":{"Freud's":{"probability":1,"occurences":1}},"clinic.":{"From":{"probability":1,"occurences":1}},"für":{"psychoanalytische":{"probability":0.33333333333333,"occurences":3},"Psychoanalyse":{"probability":0.33333333333333,"occurences":3},"Psychoanalyse,":{"probability":0.33333333333333,"occurences":3}},"writings,":{"and":{"probability":1,"occurences":1}},"members":{"whom":{"probability":0.5,"occurences":2},"would":{"probability":0.5,"occurences":2}},"Carl":{"Gustav":{"probability":0.5,"occurences":2},"Jung,":{"probability":0.5,"occurences":2}},"University":{"of":{"probability":0.75,"occurences":12},"medical":{"probability":0.25,"occurences":4}},"early":{"Freud":{"probability":0.16666666666667,"occurences":6},"childhood":{"probability":0.16666666666667,"occurences":6},"meetings":{"probability":0.16666666666667,"occurences":6},"followers":{"probability":0.16666666666667,"occurences":6},"clinical":{"probability":0.33333333333333,"occurences":12}},"She":{"was":{"probability":1,"occurences":1}},"woman":{"member,":{"probability":1,"occurences":1}},"wish-fulfillments":{"made":{"probability":1,"occurences":1}},"cavity":{"–":{"probability":1,"occurences":1}},"prestige":{"thus":{"probability":0.5,"occurences":2},"it":{"probability":0.5,"occurences":2}},"psychological":{"investigation":{"probability":1,"occurences":1}},"manifested":{"anxiety":{"probability":1,"occurences":1}},"capacity":{"to":{"probability":1,"occurences":1}},"Shakespeare's":{"plays.":{"probability":1,"occurences":1}},"periodicity":{"and":{"probability":1,"occurences":1}},"\"nasal":{"reflex":{"probability":1,"occurences":1}},"organize":{"the":{"probability":1,"occurences":1}},"their":{"ambitions":{"probability":0.125,"occurences":8},"Relation":{"probability":0.125,"occurences":8},"subsequent":{"probability":0.125,"occurences":8},"onset.":{"probability":0.125,"occurences":8},"attendance":{"probability":0.125,"occurences":8},"relationship":{"probability":0.25,"occurences":16},"common":{"probability":0.125,"occurences":8}},"ear,":{"Freud":{"probability":0.25,"occurences":1},"his":{"probability":0.25,"occurences":1},"nose":{"probability":0.25,"occurences":1},"tasked":{"probability":0.25,"occurences":1}},"specialized":{"in":{"probability":1,"occurences":1}},"Case":{"of":{"probability":1,"occurences":1}},"addition":{"to":{"probability":1,"occurences":1}},"However,":{"Freud's":{"probability":1,"occurences":1}},"Hamburg.":{"They":{"probability":1,"occurences":1}},"disastrous":{"role":{"probability":1,"occurences":1}},"term":{"\"psychoanalysis\"":{"probability":1,"occurences":1}},"neuroses,\"":{"primarily":{"probability":1,"occurences":1}},"psychogenetic":{"origins":{"probability":1,"occurences":1}},"endorsement":{"of":{"probability":1,"occurences":1}},"American":{"academic":{"probability":0.5,"occurences":2},"Psychoanalytic":{"probability":0.5,"occurences":2}},"1891.":{"Over":{"probability":1,"occurences":1}},"Dreams.":{"Freud":{"probability":1,"occurences":1}},"\"polymorphous":{"perverse\"":{"probability":1,"occurences":1}},"intellect":{"among":{"probability":1,"occurences":1}},"radical":{"new":{"probability":1,"occurences":1}},"sister-in-law,":{"has":{"probability":1,"occurences":1}},"valuable":{"painting.":{"probability":1,"occurences":1}},"possible":{"existence":{"probability":1,"occurences":1}},"medical":{"psychopathology":{"probability":0.2,"occurences":5},"school.":{"probability":0.2,"occurences":5},"life.":{"probability":0.2,"occurences":5},"career":{"probability":0.2,"occurences":5},"textbook,":{"probability":0.2,"occurences":5}},"hostility":{"to":{"probability":1,"occurences":1}},"\"psychoanalysis\"":{"to":{"probability":1,"occurences":1}},"marked":{"the":{"probability":1,"occurences":4}},"phrase":{"\"talking":{"probability":1,"occurences":1}},"Vienna.":{"In":{"probability":1,"occurences":9}},"successful":{"treatment":{"probability":1,"occurences":1}},"did":{"not":{"probability":1,"occurences":4}},"potential":{"social":{"probability":1,"occurences":1}},"Josef":{"Breuer,":{"probability":0.5,"occurences":2},"Breuer).":{"probability":0.5,"occurences":2}},"1885,":{"a":{"probability":0.5,"occurences":2},"Freud":{"probability":0.5,"occurences":2}},"neuroses.":{"On":{"probability":1,"occurences":1}},"demonstrated":{"with":{"probability":0.5,"occurences":2},"in":{"probability":0.5,"occurences":2}},"have":{"read":{"probability":0.33333333333333,"occurences":3},"been":{"probability":0.33333333333333,"occurences":3},"a":{"probability":0.33333333333333,"occurences":3}},"\"dream":{"work\".":{"probability":1,"occurences":1}},"hope":{"of":{"probability":1,"occurences":1}},"Salzburg":{"on":{"probability":0.5,"occurences":2},"Congress":{"probability":0.5,"occurences":2}},"There":{"was":{"probability":0.5,"occurences":2},"were,":{"probability":0.5,"occurences":2}},"System":{"at":{"probability":1,"occurences":1}},"great":{"quantities.":{"probability":0.5,"occurences":2},"habit.\"":{"probability":0.5,"occurences":2}},"Burghölzli":{"Mental":{"probability":0.5,"occurences":2},"and":{"probability":0.5,"occurences":2}},"practicing":{"analysts.\"":{"probability":1,"occurences":1}},"said":{"he":{"probability":1,"occurences":1}},"scientific":{"research":{"probability":1,"occurences":1}},"develop":{"radical":{"probability":1,"occurences":1}},"review":{"of":{"probability":0.5,"occurences":2},"in":{"probability":0.5,"occurences":2}},"retreat":{"where":{"probability":1,"occurences":1}},"employed":{"as":{"probability":1,"occurences":1}},"yet":{"opened":{"probability":1,"occurences":1}},"seventeen.":{"In":{"probability":1,"occurences":1}},"form":{"the":{"probability":0.5,"occurences":1},"for":{"probability":0.5,"occurences":1}},"1891),":{"Ernst":{"probability":1,"occurences":1}},"theoretical":{"mainstream":{"probability":0.33333333333333,"occurences":3},"model":{"probability":0.33333333333333,"occurences":3},"and":{"probability":0.33333333333333,"occurences":3}},"remained":{"a":{"probability":1,"occurences":1}},"New":{"York-based":{"probability":0.5,"occurences":2},"York":{"probability":0.5,"occurences":2}},"autumn":{"of":{"probability":1,"occurences":1}},"analysts.\"":{"In":{"probability":1,"occurences":1}},"Freud,":{"her":{"probability":0.5,"occurences":2},"who":{"probability":0.5,"occurences":2}},"(supposedly)":{"had":{"probability":1,"occurences":1}},"affair.":{"The":{"probability":0.5,"occurences":2},"Freud":{"probability":0.5,"occurences":2}},"Saturday":{"evening":{"probability":1,"occurences":4}},"either":{"case":{"probability":1,"occurences":1}},"heretofore":{"prevailing":{"probability":1,"occurences":1}},"conversion":{"to":{"probability":1,"occurences":1}},"reconstituted":{"as":{"probability":1,"occurences":1}},"They":{"maintained":{"probability":0.25,"occurences":4},"had":{"probability":0.5,"occurences":8},"would":{"probability":0.25,"occurences":4}},"eventually":{"suffering":{"probability":0.33333333333333,"occurences":3},"led":{"probability":0.33333333333333,"occurences":3},"abandoned":{"probability":0.33333333333333,"occurences":3}},"Putnam,":{"Professor":{"probability":1,"occurences":1}},"Jung.":{"Both":{"probability":0.5,"occurences":2},"This":{"probability":0.5,"occurences":2}},"Adler,":{"Max":{"probability":0.5,"occurences":2},"regarded":{"probability":0.5,"occurences":2}},"history":{"of":{"probability":1,"occurences":4}},"founded":{"in":{"probability":0.5,"occurences":8},"the":{"probability":0.25,"occurences":4},"this":{"probability":0.25,"occurences":4}},"transition":{"from":{"probability":1,"occurences":1}},"Sándor":{"Ferenczi":{"probability":1,"occurences":1}},"action":{"of":{"probability":1,"occurences":1}},"platform":{"for":{"probability":1,"occurences":1}},"married":{"Martha":{"probability":1,"occurences":1}},"cocaine":{"in":{"probability":1,"occurences":1}},"Breuer,":{"in":{"probability":1,"occurences":1}},"minister":{"of":{"probability":1,"occurences":1}},"daily":{"newspaper":{"probability":1,"occurences":1}},"setting":{"in":{"probability":1,"occurences":1}},"already":{"an":{"probability":1,"occurences":1}},"blame\",":{"as":{"probability":1,"occurences":1}},"residue":{"of":{"probability":1,"occurences":1}},"mobility":{"and":{"probability":1,"occurences":1}},"Silberstein,":{"that":{"probability":1,"occurences":1}},"effects":{"of":{"probability":1,"occurences":4}},"implemented":{"at":{"probability":1,"occurences":1}},"respect.":{"Freud":{"probability":1,"occurences":1}},"prematurely":{"in":{"probability":1,"occurences":1}},"themselves":{"as":{"probability":1,"occurences":1}},"it.":{"Despite":{"probability":1,"occurences":1}},"work,":{"Freud":{"probability":0.66666666666667,"occurences":6},"had":{"probability":0.33333333333333,"occurences":3}},"Practicing":{"Physicians,":{"probability":1,"occurences":1}},"haemorrhages":{"were":{"probability":1,"occurences":1}},"near":{"Innere":{"probability":1,"occurences":1}},"clinical":{"method":{"probability":0.16666666666667,"occurences":24},"work.":{"probability":0.33333333333333,"occurences":48},"work":{"probability":0.083333333333333,"occurences":12},"setting":{"probability":0.083333333333333,"occurences":12},"practice.":{"probability":0.083333333333333,"occurences":12},"work,":{"probability":0.083333333333333,"occurences":12},"and":{"probability":0.16666666666667,"occurences":24}},"close":{"relationship":{"probability":1,"occurences":1}},"attributed":{"to":{"probability":1,"occurences":1}},"paid":{"secretary.":{"probability":1,"occurences":1}},"member":{"of":{"probability":1,"occurences":1}},"studied":{"medicine":{"probability":1,"occurences":1}},"emotional":{"support":{"probability":1,"occurences":1}},"1901.":{"In":{"probability":0.5,"occurences":2},"He":{"probability":0.5,"occurences":2}},"again":{"in":{"probability":1,"occurences":4}},"Wilhelm":{"Fliess,":{"probability":0.66666666666667,"occurences":6},"Stekel.":{"probability":0.33333333333333,"occurences":3}},"Nietzsche,":{"both":{"probability":1,"occurences":1}},"psychiatrist,":{"travelled":{"probability":1,"occurences":1}},"1898,":{"signed":{"probability":1,"occurences":1}},"issues":{"relating":{"probability":1,"occurences":1}},"readership,":{"Freud":{"probability":1,"occurences":1}},"combination":{"of":{"probability":1,"occurences":1}},"After":{"a":{"probability":0.5,"occurences":2},"Fliess":{"probability":0.5,"occurences":2}},"previous":{"year":{"probability":1,"occurences":1}},"Putnam's":{"subsequent":{"probability":1,"occurences":1}},"rivalrous":{"jealousy":{"probability":1,"occurences":1}},"historical":{"district":{"probability":1,"occurences":1}},"smoker.":{"He":{"probability":1,"occurences":1}},"(b.":{"1887),":{"probability":0.16666666666667,"occurences":6},"1892),":{"probability":0.16666666666667,"occurences":6},"1895).":{"probability":0.16666666666667,"occurences":6},"1891),":{"probability":0.16666666666667,"occurences":6},"1893),":{"probability":0.16666666666667,"occurences":6},"1889),":{"probability":0.16666666666667,"occurences":6}},"hysterical":{"\"wish-bleedings\"":{"probability":1,"occurences":1}},"Stekel,":{"in":{"probability":1,"occurences":1}},"illness\"":{"and":{"probability":1,"occurences":1}},"took":{"place":{"probability":1,"occurences":1}},"reasons":{"and":{"probability":1,"occurences":1}},"condoms":{"–":{"probability":1,"occurences":1}},"initially":{"a":{"probability":1,"occurences":1}},"although":{"still":{"probability":1,"occurences":1}},"impasse,":{"as":{"probability":1,"occurences":1}},"\"self-analysis\"":{"of":{"probability":1,"occurences":1}},"enhanced,":{"Freud":{"probability":1,"occurences":1}},"pains":{"with":{"probability":0.5,"occurences":2},"were,":{"probability":0.5,"occurences":2}},"details":{"of":{"probability":1,"occurences":1}},"irregularities,":{"disturbing":{"probability":1,"occurences":1}},"hypnosis.":{"He":{"probability":1,"occurences":1}},"believed":{"smoking":{"probability":1,"occurences":1}},"Bauernmarkt,":{"in":{"probability":1,"occurences":1}},"underrated":{"significance":{"probability":1,"occurences":1}},"Isaac":{"Bernays,":{"probability":1,"occurences":1}},"nasal":{"bleeding":{"probability":0.25,"occurences":3},"reflex":{"probability":0.25,"occurences":3},"cavity":{"probability":0.25,"occurences":3},"(and":{"probability":0.25,"occurences":3}},"Fechner":{"and":{"probability":1,"occurences":1}},"latter's":{"Psychology":{"probability":1,"occurences":1}},"ordinarius\"":{"was":{"probability":0.5,"occurences":1},"in":{"probability":0.5,"occurences":1}},"conferred,":{"there":{"probability":1,"occurences":1}},"smoking":{"tobacco":{"probability":0.5,"occurences":2},"enhanced":{"probability":0.5,"occurences":2}},"etiology":{"of":{"probability":1,"occurences":1}},"Although":{"Brentano":{"probability":1,"occurences":1}},"curable":{"by":{"probability":1,"occurences":1}},"Brentano,":{"who":{"probability":1,"occurences":1}},"Analysis":{"of":{"probability":1,"occurences":1}},"Though":{"Freud":{"probability":1,"occurences":1}},"began":{"his":{"probability":0.2,"occurences":5},"to":{"probability":0.2,"occurences":5},"smoking":{"probability":0.2,"occurences":5},"using":{"probability":0.2,"occurences":5},"a":{"probability":0.2,"occurences":5}},"Scientific":{"Psychology":{"probability":1,"occurences":1}},"written":{"a":{"probability":1,"occurences":1}},"Brentano":{"denied":{"probability":0.5,"occurences":2},"discussed":{"probability":0.5,"occurences":2}},"desire":{"by":{"probability":1,"occurences":1}},"work":{"were":{"probability":0.083333333333333,"occurences":12},"and":{"probability":0.25,"occurences":36},"which,":{"probability":0.083333333333333,"occurences":12},"in":{"probability":0.083333333333333,"occurences":12},"on":{"probability":0.083333333333333,"occurences":12},"represented":{"probability":0.083333333333333,"occurences":12},"eventually":{"probability":0.083333333333333,"occurences":12},"began":{"probability":0.083333333333333,"occurences":12},"of":{"probability":0.16666666666667,"occurences":24}},"retrieved":{"memories":{"probability":1,"occurences":1}},"importance":{"of":{"probability":0.33333333333333,"occurences":3},"to":{"probability":0.33333333333333,"occurences":3},"in":{"probability":0.33333333333333,"occurences":3}},"identity.":{"The":{"probability":1,"occurences":1}},"1889),":{"Oliver":{"probability":1,"occurences":1}},"tobacco":{"at":{"probability":1,"occurences":1}},"Richard":{"von":{"probability":1,"occurences":1}},"institute":{"of":{"probability":1,"occurences":1}},"cigarette":{"smoker,":{"probability":1,"occurences":1}},"continued":{"her":{"probability":0.5,"occurences":2},"with":{"probability":0.5,"occurences":2}},"positive":{"review":{"probability":1,"occurences":1}},"at":{"where":{"probability":0.019607843137255,"occurences":25},"habit.\"":{"probability":0.019607843137255,"occurences":25},"in":{"probability":0.019607843137255,"occurences":25},"Nietzsche's":{"probability":0.019607843137255,"occurences":25},"patients'":{"probability":0.019607843137255,"occurences":25},"the":{"probability":0.25490196078431,"occurences":325},"more":{"probability":0.019607843137255,"occurences":25},"room.":{"probability":0.019607843137255,"occurences":25},"were":{"probability":0.019607843137255,"occurences":25},"Fliess":{"probability":0.019607843137255,"occurences":25},"remains":{"probability":0.019607843137255,"occurences":25},"unconscious":{"probability":0.019607843137255,"occurences":25},"to":{"probability":0.019607843137255,"occurences":25},"last":{"probability":0.019607843137255,"occurences":25},"he":{"probability":0.03921568627451,"occurences":50},"of":{"probability":0.019607843137255,"occurences":25},"Freud's":{"probability":0.019607843137255,"occurences":25},"Zurich":{"probability":0.019607843137255,"occurences":25},"quantities.":{"probability":0.019607843137255,"occurences":25},"addictions,":{"probability":0.019607843137255,"occurences":25},"age":{"probability":0.019607843137255,"occurences":25},"specialist":{"probability":0.019607843137255,"occurences":25},"presupposes":{"probability":0.019607843137255,"occurences":25},"every":{"probability":0.019607843137255,"occurences":25},"it":{"probability":0.019607843137255,"occurences":25},"his":{"probability":0.058823529411765,"occurences":75},"Harvard,":{"probability":0.019607843137255,"occurences":25},"Freud":{"probability":0.019607843137255,"occurences":25},"Berggasse":{"probability":0.03921568627451,"occurences":50},"which":{"probability":0.019607843137255,"occurences":25},"\"nasal":{"probability":0.019607843137255,"occurences":25},"a":{"probability":0.058823529411765,"occurences":75},"infantile":{"probability":0.019607843137255,"occurences":25}},"them.":{"Freud":{"probability":0.5,"occurences":2},"In":{"probability":0.5,"occurences":2}},"throughout":{"his":{"probability":1,"occurences":1}},"specialist":{"whom":{"probability":1,"occurences":1}},"1902,":{"Freud":{"probability":0.5,"occurences":2},"a":{"probability":0.5,"occurences":2}},"depth":{"interpretation":{"probability":1,"occurences":1}},"between":{"Freud":{"probability":0.33333333333333,"occurences":3},"his":{"probability":0.33333333333333,"occurences":3},"neurology":{"probability":0.33333333333333,"occurences":3}},"Empirical":{"Standpoint":{"probability":1,"occurences":1}},"event,":{"at":{"probability":1,"occurences":1}},"Wiener":{"Tagblatt.":{"probability":1,"occurences":1}},"conference":{"the":{"probability":1,"occurences":1}},"granddaughter":{"of":{"probability":1,"occurences":1}},"new":{"theories":{"probability":0.5,"occurences":8},"prophet":{"probability":0.25,"occurences":4},"clinical":{"probability":0.25,"occurences":4}},"cigar":{"and":{"probability":0.5,"occurences":2},"smoker.":{"probability":0.5,"occurences":2}},"Massachusetts,":{"where":{"probability":1,"occurences":1}},"necessary":{"precondition":{"probability":1,"occurences":1}},"psychoanalytic":{"methods":{"probability":0.11111111111111,"occurences":9},"insights":{"probability":0.11111111111111,"occurences":9},"ideas,":{"probability":0.11111111111111,"occurences":9},"group":{"probability":0.11111111111111,"occurences":9},"method.":{"probability":0.11111111111111,"occurences":9},"movement":{"probability":0.11111111111111,"occurences":9},"movement.":{"probability":0.11111111111111,"occurences":9},"cause":{"probability":0.22222222222222,"occurences":18}},"ex-patients,":{"a":{"probability":1,"occurences":1}},"At":{"first,":{"probability":1,"occurences":1}},"coin":{"the":{"probability":1,"occurences":1}},"existing":{"theory,":{"probability":1,"occurences":1}},"consumed":{"in":{"probability":1,"occurences":1}},"content":{"of":{"probability":1,"occurences":1}},"beginnings":{"of":{"probability":1,"occurences":1}},"abandon":{"hypnosis,":{"probability":1,"occurences":1}},"rights":{"for":{"probability":1,"occurences":1}},"human":{"biorhythms":{"probability":0.5,"occurences":2},"psychology":{"probability":0.5,"occurences":2}},"member,":{"Margarete":{"probability":1,"occurences":1}},"be":{"achieved":{"probability":0.076923076923077,"occurences":12},"granted":{"probability":0.076923076923077,"occurences":12},"studied.\"":{"probability":0.076923076923077,"occurences":12},"resisted":{"probability":0.076923076923077,"occurences":12},"transformative":{"probability":0.076923076923077,"occurences":12},"taken":{"probability":0.076923076923077,"occurences":12},"fruitfully":{"probability":0.076923076923077,"occurences":12},"the":{"probability":0.15384615384615,"occurences":24},"of":{"probability":0.076923076923077,"occurences":12},"traced":{"probability":0.076923076923077,"occurences":12},"made":{"probability":0.076923076923077,"occurences":12},"loved":{"probability":0.076923076923077,"occurences":12}},"(1901)":{"and":{"probability":1,"occurences":1}},"Plans":{"for":{"probability":1,"occurences":1}},"signed":{"his":{"probability":0.5,"occurences":1},"by":{"probability":0.5,"occurences":1}},"formed":{"with":{"probability":1,"occurences":1}},"travelling":{"with":{"probability":1,"occurences":1}},"course":{"of":{"probability":1,"occurences":1}},"psychotherapy":{"institute":{"probability":1,"occurences":1}},"division":{"of":{"probability":1,"occurences":1}},"elected,":{"with":{"probability":1,"occurences":1}},"existence,":{"his":{"probability":1,"occurences":1}},"reluctant":{"to":{"probability":1,"occurences":1}},"Zürich":{"University":{"probability":0.33333333333333,"occurences":3},"to":{"probability":0.33333333333333,"occurences":3},"contingents":{"probability":0.33333333333333,"occurences":3}},"birth.":{"Both":{"probability":1,"occurences":1}},"interlocutor.":{"However,":{"probability":1,"occurences":1}},"Three":{"Essays":{"probability":1,"occurences":4}},"relinquished":{"in":{"probability":1,"occurences":1}},"private":{"practice":{"probability":1,"occurences":4}},"self-cutting":{"and":{"probability":1,"occurences":1}},"imagined":{"and":{"probability":1,"occurences":1}},"plays.":{"Freud's":{"probability":1,"occurences":1}},"13":{"August":{"probability":0.5,"occurences":1},"by":{"probability":0.5,"occurences":1}},"1908.":{"This":{"probability":1,"occurences":1}},"abandoned":{"the":{"probability":0.5,"occurences":2},"after":{"probability":0.5,"occurences":2}},"concluding":{"chapter":{"probability":1,"occurences":1}},"word-association":{"and":{"probability":1,"occurences":1}},"trauma":{"as":{"probability":1,"occurences":1}},"law\".":{"In":{"probability":1,"occurences":1}},"than":{"to":{"probability":1,"occurences":1}},"society:":{"The":{"probability":1,"occurences":1}},"apartment":{"every":{"probability":0.5,"occurences":2},"at":{"probability":0.5,"occurences":2}},"Later,":{"he":{"probability":1,"occurences":1}},"win":{"him":{"probability":1,"occurences":1}},"Over":{"a":{"probability":1,"occurences":1}},"Breuer).":{"In":{"probability":1,"occurences":1}},"substitutes":{"for":{"probability":1,"occurences":1}},"status":{"of":{"probability":1,"occurences":1}},"but":{"one":{"probability":0.5,"occurences":2},"it":{"probability":0.5,"occurences":2}},"analyzed":{"to":{"probability":1,"occurences":1}},"biology\",":{"later":{"probability":1,"occurences":1}},"suggestion":{"of":{"probability":1,"occurences":4}},"United":{"States,":{"probability":0.5,"occurences":2},"States.":{"probability":0.5,"occurences":2}},"permanently":{"disfigured.":{"probability":1,"occurences":1}},"the":{"approach":{"probability":0.0056179775280899,"occurences":173},"middle":{"probability":0.0056179775280899,"occurences":173},"latter's":{"probability":0.0056179775280899,"occurences":173},"Congress.":{"probability":0.0056179775280899,"occurences":173},"term":{"probability":0.0056179775280899,"occurences":173},"tissue":{"probability":0.0056179775280899,"occurences":173},"psychogenetic":{"probability":0.0056179775280899,"occurences":173},"American":{"probability":0.0056179775280899,"occurences":173},"light":{"probability":0.0056179775280899,"occurences":173},"discussion":{"probability":0.01123595505618,"occurences":346},"worldwide":{"probability":0.0056179775280899,"occurences":173},"words":{"probability":0.0056179775280899,"occurences":173},"enhanced":{"probability":0.0056179775280899,"occurences":173},"importance":{"probability":0.0056179775280899,"occurences":173},"main":{"probability":0.0056179775280899,"occurences":173},"possible":{"probability":0.0056179775280899,"occurences":173},"nature":{"probability":0.0056179775280899,"occurences":173},"Kepler":{"probability":0.0056179775280899,"occurences":173},"publication":{"probability":0.0056179775280899,"occurences":173},"Vienna":{"probability":0.01123595505618,"occurences":346},"monthly":{"probability":0.0056179775280899,"occurences":173},"Wednesday":{"probability":0.02247191011236,"occurences":692},"neuroses.":{"probability":0.0056179775280899,"occurences":173},"granddaughter":{"probability":0.0056179775280899,"occurences":173},"field":{"probability":0.0056179775280899,"occurences":173},"\"dream":{"probability":0.0056179775280899,"occurences":173},"hope":{"probability":0.0056179775280899,"occurences":173},"Salzburg":{"probability":0.0056179775280899,"occurences":173},"Freud":{"probability":0.0056179775280899,"occurences":173},"psychoanalytic":{"probability":0.01685393258427,"occurences":519},"Untimely":{"probability":0.0056179775280899,"occurences":173},"invitation":{"probability":0.0056179775280899,"occurences":173},"Burghölzli":{"probability":0.01123595505618,"occurences":346},"Nuremberg":{"probability":0.0056179775280899,"occurences":173},"Aphasias:":{"probability":0.0056179775280899,"occurences":173},"Project":{"probability":0.0056179775280899,"occurences":173},"director":{"probability":0.01123595505618,"occurences":346},"Internationale":{"probability":0.0056179775280899,"occurences":173},"mind,":{"probability":0.0056179775280899,"occurences":173},"beginnings":{"probability":0.0056179775280899,"occurences":173},"theoretical":{"probability":0.0056179775280899,"occurences":173},"Jahrbuch":{"probability":0.0056179775280899,"occurences":173},"New":{"probability":0.01123595505618,"occurences":346},"psychoneuroses":{"probability":0.0056179775280899,"occurences":173},"table":{"probability":0.0056179775280899,"occurences":173},"autumn":{"probability":0.0056179775280899,"occurences":173},"affair.":{"probability":0.0056179775280899,"occurences":173},"heretofore":{"probability":0.0056179775280899,"occurences":173},"one":{"probability":0.0056179775280899,"occurences":173},"course":{"probability":0.0056179775280899,"occurences":173},"year,":{"probability":0.0056179775280899,"occurences":173},"Russian":{"probability":0.0056179775280899,"occurences":173},"theory":{"probability":0.01685393258427,"occurences":519},"Unconscious":{"probability":0.01123595505618,"occurences":346},"Galvanic":{"probability":0.0056179775280899,"occurences":173},"basis":{"probability":0.01685393258427,"occurences":519},"concepts":{"probability":0.0056179775280899,"occurences":173},"Congress":{"probability":0.0056179775280899,"occurences":173},"study":{"probability":0.0056179775280899,"occurences":173},"Oedipus":{"probability":0.0056179775280899,"occurences":173},"concluding":{"probability":0.0056179775280899,"occurences":173},"minister":{"probability":0.0056179775280899,"occurences":173},"Viennese":{"probability":0.01123595505618,"occurences":346},"post":{"probability":0.0056179775280899,"occurences":173},"impact":{"probability":0.0056179775280899,"occurences":173},"French":{"probability":0.0056179775280899,"occurences":173},"society:":{"probability":0.0056179775280899,"occurences":173},"residue":{"probability":0.0056179775280899,"occurences":173},"following":{"probability":0.0056179775280899,"occurences":173},"experience":{"probability":0.0056179775280899,"occurences":173},"effects":{"probability":0.0056179775280899,"occurences":173},"completion":{"probability":0.0056179775280899,"occurences":173},"plagiarism":{"probability":0.0056179775280899,"occurences":173},"theories":{"probability":0.0056179775280899,"occurences":173},"ritual":{"probability":0.0056179775280899,"occurences":173},"face-saving":{"probability":0.0056179775280899,"occurences":173},"nose":{"probability":0.0056179775280899,"occurences":173},"clinical":{"probability":0.01123595505618,"occurences":346},"subject":{"probability":0.0056179775280899,"occurences":173},"death":{"probability":0.01123595505618,"occurences":346},"recognition":{"probability":0.0056179775280899,"occurences":173},"bounding":{"probability":0.0056179775280899,"occurences":173},"suggestion":{"probability":0.01123595505618,"occurences":346},"United":{"probability":0.01123595505618,"occurences":346},"atmosphere":{"probability":0.0056179775280899,"occurences":173},"complex":{"probability":0.0056179775280899,"occurences":173},"hospital.":{"probability":0.0056179775280899,"occurences":173},"Zürich":{"probability":0.0056179775280899,"occurences":173},"physician":{"probability":0.0056179775280899,"occurences":173},"repression":{"probability":0.0056179775280899,"occurences":173},"group":{"probability":0.0056179775280899,"occurences":173},"concept.":{"probability":0.0056179775280899,"occurences":173},"palliative":{"probability":0.0056179775280899,"occurences":173},"foundation":{"probability":0.0056179775280899,"occurences":173},"potential":{"probability":0.0056179775280899,"occurences":173},"university's":{"probability":0.0056179775280899,"occurences":173},"use":{"probability":0.0056179775280899,"occurences":173},"political":{"probability":0.0056179775280899,"occurences":173},"evolution":{"probability":0.0056179775280899,"occurences":173},"same":{"probability":0.02247191011236,"occurences":692},"tailoring":{"probability":0.0056179775280899,"occurences":173},"mid-1880s":{"probability":0.0056179775280899,"occurences":173},"\"actual":{"probability":0.0056179775280899,"occurences":173},"practice":{"probability":0.0056179775280899,"occurences":173},"members":{"probability":0.0056179775280899,"occurences":173},"previous":{"probability":0.0056179775280899,"occurences":173},"application":{"probability":0.0056179775280899,"occurences":173},"intellectual":{"probability":0.0056179775280899,"occurences":173},"content":{"probability":0.0056179775280899,"occurences":173},"psychic":{"probability":0.0056179775280899,"occurences":173},"intervention":{"probability":0.0056179775280899,"occurences":173},"University":{"probability":0.01685393258427,"occurences":519},"early":{"probability":0.01123595505618,"occurences":346},"formation":{"probability":0.01123595505618,"occurences":346},"Society":{"probability":0.0056179775280899,"occurences":173},"phrase":{"probability":0.0056179775280899,"occurences":173},"matter":{"probability":0.0056179775280899,"occurences":173},"lecture":{"probability":0.0056179775280899,"occurences":173},"remedial":{"probability":0.0056179775280899,"occurences":173},"unconscious,":{"probability":0.0056179775280899,"occurences":173},"regular":{"probability":0.0056179775280899,"occurences":173},"university,":{"probability":0.0056179775280899,"occurences":173},"group's":{"probability":0.0056179775280899,"occurences":173},"Nervous":{"probability":0.0056179775280899,"occurences":173},"distinguished":{"probability":0.0056179775280899,"occurences":173},"year":{"probability":0.0056179775280899,"occurences":173},"editorship":{"probability":0.0056179775280899,"occurences":173},"translation":{"probability":0.0056179775280899,"occurences":173},"English-speaking":{"probability":0.0056179775280899,"occurences":173},"functioning":{"probability":0.0056179775280899,"occurences":173},"founding":{"probability":0.0056179775280899,"occurences":173},"way":{"probability":0.0056179775280899,"occurences":173},"\"drives\",":{"probability":0.0056179775280899,"occurences":173},"prevailing":{"probability":0.0056179775280899,"occurences":173},"Hotel":{"probability":0.0056179775280899,"occurences":173},"origin":{"probability":0.0056179775280899,"occurences":173},"Theory":{"probability":0.01123595505618,"occurences":346},"first":{"probability":0.028089887640449,"occurences":865},"work":{"probability":0.01123595505618,"occurences":346},"unconscious":{"probability":0.01685393258427,"occurences":519},"subsequent":{"probability":0.0056179775280899,"occurences":173},"etiology":{"probability":0.0056179775280899,"occurences":173},"most":{"probability":0.0056179775280899,"occurences":173}},"way,":{"her":{"probability":1,"occurences":1}},"allegiance":{"to":{"probability":1,"occurences":1}},"whilst":{"travelling":{"probability":1,"occurences":1}},"repeatedly":{"operate":{"probability":1,"occurences":1}},"support":{"of":{"probability":0.5,"occurences":2},"from":{"probability":0.5,"occurences":2}},"Shakespeare":{"in":{"probability":1,"occurences":1}},"1898":{"had":{"probability":1,"occurences":1}},"mind":{"in":{"probability":1,"occurences":1}},"Anna":{"(b.":{"probability":0.5,"occurences":2},"O.,":{"probability":0.5,"occurences":2}},"spread":{"the":{"probability":0.5,"occurences":1},"media":{"probability":0.5,"occurences":1}},"him":{"of":{"probability":0.16666666666667,"occurences":6},"toward":{"probability":0.16666666666667,"occurences":6},"to":{"probability":0.5,"occurences":18},"a":{"probability":0.16666666666667,"occurences":6}},"texts":{"of":{"probability":0.5,"occurences":2},"to":{"probability":0.5,"occurences":2}},"hospital":{"post":{"probability":1,"occurences":1}},"about":{"her":{"probability":0.5,"occurences":2},"whatever":{"probability":0.5,"occurences":2}},"(and":{"menstrual)":{"probability":1,"occurences":1}},"Budapest":{"and":{"probability":1,"occurences":1}},"The":{"same":{"probability":0.11111111111111,"occurences":36},"Interpretation":{"probability":0.16666666666667,"occurences":54},"other":{"probability":0.055555555555556,"occurences":18},"Psychopathology":{"probability":0.055555555555556,"occurences":18},"Birth":{"probability":0.055555555555556,"occurences":18},"last":{"probability":0.055555555555556,"occurences":18},"inconsistent":{"probability":0.055555555555556,"occurences":18},"close":{"probability":0.055555555555556,"occurences":18},"first":{"probability":0.055555555555556,"occurences":18},"title":{"probability":0.055555555555556,"occurences":18},"discovery":{"probability":0.055555555555556,"occurences":18},"treatment":{"probability":0.055555555555556,"occurences":18},"event,":{"probability":0.055555555555556,"occurences":18},"Philosophy":{"probability":0.055555555555556,"occurences":18},"gatherings":{"probability":0.055555555555556,"occurences":18}},"Tragedy":{"and":{"probability":1,"occurences":1}},"appear":{"superficial.":{"probability":0.5,"occurences":2},"from":{"probability":0.5,"occurences":2}},"(he":{"would":{"probability":1,"occurences":1}},"partially":{"derived":{"probability":1,"occurences":1}},"Tatiana":{"Rosenthal":{"probability":1,"occurences":1}},"acclaimed":{"researcher":{"probability":1,"occurences":1}},"speculations":{"on":{"probability":1,"occurences":1}},"other":{"three":{"probability":1,"occurences":1}},"addictions,":{"including":{"probability":1,"occurences":1}},"1896,":{"Minna":{"probability":1,"occurences":1}},"textbook,":{"Outline":{"probability":1,"occurences":1}},"graduates":{"of":{"probability":1,"occurences":1}},"hoped":{"to":{"probability":1,"occurences":1}},"duties":{"attached":{"probability":1,"occurences":1}},"traced":{"back":{"probability":1,"occurences":1}},"increasingly":{"critical":{"probability":1,"occurences":1}},"Hospital":{"in":{"probability":1,"occurences":1}},"1909.":[],"Doctorate,":{"marked":{"probability":1,"occurences":1}},"which,":{"as":{"probability":0.25,"occurences":4},"he":{"probability":0.25,"occurences":4},"following":{"probability":0.25,"occurences":4},"since":{"probability":0.25,"occurences":4}},"in":{"which,":{"probability":0.0089285714285714,"occurences":99},"in":{"probability":0.017857142857143,"occurences":198},"neurology.":{"probability":0.0089285714285714,"occurences":99},"Bauernmarkt,":{"probability":0.0089285714285714,"occurences":99},"private":{"probability":0.0089285714285714,"occurences":99},"Hamburg.":{"probability":0.0089285714285714,"occurences":99},"May":{"probability":0.0089285714285714,"occurences":99},"1920).":{"probability":0.0089285714285714,"occurences":99},"an":{"probability":0.0089285714285714,"occurences":99},"subsequent":{"probability":0.0089285714285714,"occurences":99},"had":{"probability":0.0089285714285714,"occurences":99},"light":{"probability":0.0089285714285714,"occurences":99},"physically":{"probability":0.0089285714285714,"occurences":99},"front":{"probability":0.0089285714285714,"occurences":99},"severity":{"probability":0.0089285714285714,"occurences":99},"that":{"probability":0.017857142857143,"occurences":198},"to":{"probability":0.0089285714285714,"occurences":99},"terms":{"probability":0.0089285714285714,"occurences":99},"1886,":{"probability":0.0089285714285714,"occurences":99},"her":{"probability":0.0089285714285714,"occurences":99},"me.\"":{"probability":0.0089285714285714,"occurences":99},"contemporary":{"probability":0.0089285714285714,"occurences":99},"Freud's":{"probability":0.0089285714285714,"occurences":99},"1938,":{"probability":0.0089285714285714,"occurences":99},"favor":{"probability":0.0089285714285714,"occurences":99},"Theodor":{"probability":0.0089285714285714,"occurences":99},"Vienna.":{"probability":0.0089285714285714,"occurences":99},"Vienna":{"probability":0.0089285714285714,"occurences":99},"a":{"probability":0.026785714285714,"occurences":297},"nonetheless":{"probability":0.0089285714285714,"occurences":99},"Studies":{"probability":0.0089285714285714,"occurences":99},"cerebral":{"probability":0.0089285714285714,"occurences":99},"1901.":{"probability":0.017857142857143,"occurences":198},"this":{"probability":0.017857142857143,"occurences":198},"profuse,":{"probability":0.0089285714285714,"occurences":99},"1897":{"probability":0.0089285714285714,"occurences":99},"great":{"probability":0.0089285714285714,"occurences":99},"philosophy":{"probability":0.0089285714285714,"occurences":99},"1884":{"probability":0.0089285714285714,"occurences":99},"1896":{"probability":0.0089285714285714,"occurences":99},"his":{"probability":0.044642857142857,"occurences":495},"1910.":{"probability":0.0089285714285714,"occurences":99},"1891.":{"probability":0.0089285714285714,"occurences":99},"1898":{"probability":0.0089285714285714,"occurences":99},"neuropathology":{"probability":0.0089285714285714,"occurences":99},"\"nervous":{"probability":0.0089285714285714,"occurences":99},"Eckstein's":{"probability":0.0089285714285714,"occurences":99},"successive":{"probability":0.0089285714285714,"occurences":99},"him":{"probability":0.0089285714285714,"occurences":99},"1917.":{"probability":0.0089285714285714,"occurences":99},"clinical":{"probability":0.0089285714285714,"occurences":99},"North":{"probability":0.0089285714285714,"occurences":99},"English":{"probability":0.0089285714285714,"occurences":99},"Zürich.":{"probability":0.017857142857143,"occurences":198},"early":{"probability":0.0089285714285714,"occurences":99},"(b.":{"probability":0.0089285714285714,"occurences":99},"1907":{"probability":0.0089285714285714,"occurences":99},"aspects":{"probability":0.0089285714285714,"occurences":99},"1885,":{"probability":0.0089285714285714,"occurences":99},"life.":{"probability":0.0089285714285714,"occurences":99},"1900":{"probability":0.0089285714285714,"occurences":99},"1909":{"probability":0.0089285714285714,"occurences":99},"various":{"probability":0.0089285714285714,"occurences":99},"The":{"probability":0.0089285714285714,"occurences":99},"of":{"probability":0.0089285714285714,"occurences":99},"Response,":{"probability":0.0089285714285714,"occurences":99},"moderating":{"probability":0.0089285714285714,"occurences":99},"respect":{"probability":0.0089285714285714,"occurences":99},"1911":{"probability":0.0089285714285714,"occurences":99},"1913":{"probability":0.0089285714285714,"occurences":99},"1905,":{"probability":0.0089285714285714,"occurences":99},"either":{"probability":0.0089285714285714,"occurences":99},"1923":{"probability":0.0089285714285714,"occurences":99},"Charcot,":{"probability":0.0089285714285714,"occurences":99},"neurology":{"probability":0.0089285714285714,"occurences":99},"1910":{"probability":0.026785714285714,"occurences":297},"the":{"probability":0.11607142857143,"occurences":1287},"who":{"probability":0.0089285714285714,"occurences":99},"horror":{"probability":0.0089285714285714,"occurences":99},"place":{"probability":0.0089285714285714,"occurences":99},"1895":{"probability":0.0089285714285714,"occurences":99},"September":{"probability":0.0089285714285714,"occurences":99},"Dorotheergasse":{"probability":0.0089285714285714,"occurences":99},"which":{"probability":0.0089285714285714,"occurences":99},"turning":{"probability":0.0089285714285714,"occurences":99},"1906,":{"probability":0.0089285714285714,"occurences":99},"Nietzsche's":{"probability":0.0089285714285714,"occurences":99}},"translations":{"of":{"probability":1,"occurences":1}},"year.":{"His":{"probability":1,"occurences":1}},"Critical":{"Study,":{"probability":1,"occurences":1}},"Charles":{"Darwin's":{"probability":1,"occurences":1}},"Sexuality,":{"published":{"probability":1,"occurences":1}},"middle":{"turbinate.":{"probability":1,"occurences":1}},"Binswanger,":{"also":{"probability":1,"occurences":1}},"an":{"outline":{"probability":0.025641025641026,"occurences":21},"using":{"probability":0.025641025641026,"occurences":21},"acrimonious":{"probability":0.025641025641026,"occurences":21},"hour,":{"probability":0.025641025641026,"occurences":21},"concluded,":{"probability":0.025641025641026,"occurences":21},"Wilhelm":{"probability":0.025641025641026,"occurences":21},"old":{"probability":0.025641025641026,"occurences":21},"academically":{"probability":0.025641025641026,"occurences":21},"assistant":{"probability":0.025641025641026,"occurences":21},"extensive":{"probability":0.051282051282051,"occurences":42},"smoking":{"probability":0.025641025641026,"occurences":21},"audience.":{"probability":0.025641025641026,"occurences":21},"biorhythms":{"probability":0.025641025641026,"occurences":21},"end.":{"probability":0.025641025641026,"occurences":21},"Analysis":{"probability":0.025641025641026,"occurences":21},"his":{"probability":0.025641025641026,"occurences":21},"apartment":{"probability":0.025641025641026,"occurences":21},"be":{"probability":0.025641025641026,"occurences":21},"establishment":{"probability":0.025641025641026,"occurences":21},"Psychoanalytic":{"probability":0.051282051282051,"occurences":42},"out-patient":{"probability":0.025641025641026,"occurences":21},"increased":{"probability":0.025641025641026,"occurences":21},"affair.":{"probability":0.025641025641026,"occurences":21},"autonomous":{"probability":0.025641025641026,"occurences":21},"member,":{"probability":0.025641025641026,"occurences":21},"a":{"probability":0.025641025641026,"occurences":21},"international":{"probability":0.025641025641026,"occurences":21},"impasse,":{"probability":0.025641025641026,"occurences":21},"academic":{"probability":0.025641025641026,"occurences":21},"psychiatrists":{"probability":0.025641025641026,"occurences":21},"influential":{"probability":0.025641025641026,"occurences":21},"ideas":{"probability":0.025641025641026,"occurences":21},"Empirical":{"probability":0.025641025641026,"occurences":21},"psychology":{"probability":0.025641025641026,"occurences":21},"Honorary":{"probability":0.025641025641026,"occurences":21},"to":{"probability":0.051282051282051,"occurences":42}},"1893),":{"and":{"probability":1,"occurences":1}},"secretary":{"respectively.":{"probability":1,"occurences":1}},"elected":{"president":{"probability":1,"occurences":1}},"May":{"1911":{"probability":1,"occurences":1}},"after":{"the":{"probability":0.25,"occurences":4},"he":{"probability":0.25,"occurences":4},"they":{"probability":0.25,"occurences":4},"its":{"probability":0.25,"occurences":4}},"Association":{"in":{"probability":1,"occurences":1}},"invited":{"to":{"probability":0.8,"occurences":20},"Freud":{"probability":0.2,"occurences":5}},"Ferstel,":{"who":{"probability":1,"occurences":1}},"influenced":{"by":{"probability":1,"occurences":1}},"reading":{"The":{"probability":1,"occurences":1}},"founding":{"of":{"probability":1,"occurences":1}},"biorhythms":{"and":{"probability":1,"occurences":1}},"how":{"all":{"probability":1,"occurences":1}},"friend,":{"Fliess,":{"probability":1,"occurences":1}},"Eduard":{"von":{"probability":0.5,"occurences":2},"Silberstein,":{"probability":0.5,"occurences":2}},"Putnam":{"and":{"probability":1,"occurences":1}},"remains":{"mute":{"probability":1,"occurences":1}},"When":{"Putnam":{"probability":1,"occurences":1}},"States.":{"When":{"probability":1,"occurences":1}},"breakthrough":{"for":{"probability":1,"occurences":1}},"every":{"neurosis":{"probability":0.33333333333333,"occurences":3},"Wednesday":{"probability":0.33333333333333,"occurences":3},"Saturday":{"probability":0.33333333333333,"occurences":3}},"four":{"days.":{"probability":1,"occurences":1}},"Bleuler":{"at":{"probability":1,"occurences":1}},"In":{"1900,":{"probability":0.052631578947368,"occurences":19},"October":{"probability":0.052631578947368,"occurences":19},"the":{"probability":0.21052631578947,"occurences":76},"1899":{"probability":0.052631578947368,"occurences":19},"1901,":{"probability":0.052631578947368,"occurences":19},"1902,":{"probability":0.052631578947368,"occurences":19},"1896,":{"probability":0.052631578947368,"occurences":19},"1882,":{"probability":0.052631578947368,"occurences":19},"1886,":{"probability":0.052631578947368,"occurences":19},"1908,":{"probability":0.052631578947368,"occurences":19},"conjunction":{"probability":0.052631578947368,"occurences":19},"addition":{"probability":0.052631578947368,"occurences":19},"it,":{"probability":0.052631578947368,"occurences":19},"March":{"probability":0.052631578947368,"occurences":19},"Three":{"probability":0.052631578947368,"occurences":19},"works":{"probability":0.052631578947368,"occurences":19}},"held":{"extensive":{"probability":1,"occurences":1}},"last":{"realised":{"probability":0.5,"occurences":2},"and":{"probability":0.5,"occurences":2}},"prepared":{"the":{"probability":1,"occurences":1}},"country":{"retreat":{"probability":1,"occurences":1}},"Harvard,":{"who":{"probability":1,"occurences":1}},"Nervous":{"System":{"probability":1,"occurences":1}},"concerns":{"with":{"probability":1,"occurences":1}},"An":{"abridged":{"probability":1,"occurences":1}},"frequently":{"demonstrated":{"probability":1,"occurences":1}},"world.":{"Both":{"probability":1,"occurences":1}},"Professor":{"of":{"probability":1,"occurences":1}},"Jackson":{"Putnam,":{"probability":1,"occurences":1}},"James":{"Jackson":{"probability":1,"occurences":1}},"psychiatrist":{"James":{"probability":1,"occurences":1}},"Study.":{"They":{"probability":1,"occurences":1}},"distinguished":{"neurologist":{"probability":1,"occurences":1}},"freely,":{"without":{"probability":1,"occurences":1}},"interest.":{"Freud's":{"probability":1,"occurences":1}},"media":{"interest.":{"probability":1,"occurences":1}},"renowned":{"neurologist":{"probability":1,"occurences":1}},"theory,":{"Freud":{"probability":1,"occurences":1}},"prior":{"philosophical":{"probability":1,"occurences":1}},"Zürich.":{"In":{"probability":1,"occurences":4}},"public":{"recognition":{"probability":0.5,"occurences":2},"endorsement":{"probability":0.5,"occurences":2}},"Honorary":{"Doctorate,":{"probability":1,"occurences":1}},"Hotel":{"Bristol,":{"probability":1,"occurences":1}},"awarded":{"an":{"probability":1,"occurences":1}},"psychoanalysis.":{"The":{"probability":1,"occurences":1}},"Worcester,":{"Massachusetts,":{"probability":1,"occurences":1}},"Clark":{"University,":{"probability":1,"occurences":1}},"ideas":{"of":{"probability":0.25,"occurences":4},"or":{"probability":0.25,"occurences":4},"in":{"probability":0.25,"occurences":4},"through":{"probability":0.25,"occurences":4}},"whatever":{"ideas":{"probability":1,"occurences":1}},"Hall,":{"president":{"probability":1,"occurences":1}},"formative":{"for":{"probability":0.5,"occurences":1},"period":{"probability":0.5,"occurences":1}},"has":{"been":{"probability":1,"occurences":9}},"Stanley":{"Hall,":{"probability":1,"occurences":1}},"left":{"Vienna":{"probability":0.25,"occurences":4},"her":{"probability":0.25,"occurences":4},"a":{"probability":0.25,"occurences":4},"the":{"probability":0.25,"occurences":4}},"September":{"1909":{"probability":1,"occurences":1}},"because":{"of":{"probability":1,"occurences":1}},"Sabina":{"Spielrein":{"probability":1,"occurences":1}},"March":{"1907,":{"probability":1,"occurences":1}},"quarter":{"of":{"probability":1,"occurences":1}},"rise,":{"in":{"probability":1,"occurences":1}},"way":{"from":{"probability":0.5,"occurences":1},"for":{"probability":0.5,"occurences":1}},"today":{"considered":{"probability":1,"occurences":1}},"stage":{"in":{"probability":1,"occurences":1}},"time":{"spent":{"probability":0.5,"occurences":2},"at":{"probability":0.5,"occurences":2}},"academic":{"and":{"probability":1,"occurences":1}},"model":{"of":{"probability":1,"occurences":1}},"research":{"into":{"probability":0.33333333333333,"occurences":3},"led":{"probability":0.33333333333333,"occurences":3},"work":{"probability":0.33333333333333,"occurences":3}},"North":{"American":{"probability":1,"occurences":1}},"Freudian":{"ideas":{"probability":1,"occurences":1}},"director":{"in":{"probability":0.5,"occurences":2},"of":{"probability":0.5,"occurences":2}},"over":{"his":{"probability":0.33333333333333,"occurences":3},"publication":{"probability":0.33333333333333,"occurences":3},"a":{"probability":0.33333333333333,"occurences":3}},"establishing":{"a":{"probability":1,"occurences":1}},"writings":{"and":{"probability":0.5,"occurences":2},"\"as":{"probability":0.5,"occurences":2}},"Toronto":{"later":{"probability":1,"occurences":1}},"Fliess's":{"theories,":{"probability":0.25,"occurences":4},"speculations":{"probability":0.25,"occurences":4},"surgery":{"probability":0.25,"occurences":4},"culpability":{"probability":0.25,"occurences":4}},"education":{"with":{"probability":1,"occurences":1}},"authorities":{"and":{"probability":1,"occurences":1}},"translation":{"rights":{"probability":1,"occurences":1}},"given":{"the":{"probability":1,"occurences":1}},"agreed":{"with":{"probability":1,"occurences":1}},"labour":{"was":{"probability":1,"occurences":1}},"English-speaking":{"world.":{"probability":1,"occurences":1}},"cause":{"of":{"probability":0.33333333333333,"occurences":2},"in":{"probability":0.66666666666667,"occurences":4}},"table":{"and":{"probability":0.5,"occurences":1},"for":{"probability":0.5,"occurences":1}},"irregular":{"nasal":{"probability":1,"occurences":1}},"turned":{"to":{"probability":1,"occurences":2}},"president.":{"Freud":{"probability":1,"occurences":1}},"it":{"Freud":{"probability":0.1,"occurences":8},"conferred,":{"probability":0.1,"occurences":8},"to":{"probability":0.1,"occurences":8},"gives":{"probability":0.1,"occurences":8},"was":{"probability":0.2,"occurences":16},"marked":{"probability":0.1,"occurences":8},"did":{"probability":0.2,"occurences":16},"has":{"probability":0.1,"occurences":8}},"put":{"in":{"probability":1,"occurences":1}},"from":{"Budapest":{"probability":0.076923076923077,"occurences":13},"1909.":{"probability":0.076923076923077,"occurences":13},"the":{"probability":0.38461538461538,"occurences":65},"colleague":{"probability":0.076923076923077,"occurences":13},"Berlin,":{"probability":0.076923076923077,"occurences":13},"an":{"probability":0.076923076923077,"occurences":13},"Shakespeare's":{"probability":0.076923076923077,"occurences":13},"a":{"probability":0.076923076923077,"occurences":13},"which":{"probability":0.076923076923077,"occurences":13}},"association":{"and":{"probability":0.5,"occurences":1},"of":{"probability":0.5,"occurences":1}},"Psychoanalyse,":{"also":{"probability":1,"occurences":1}},"established":{"a":{"probability":1,"occurences":1}},"Zeitschrift":{"für":{"probability":1,"occurences":1}},"one":{"that":{"probability":0.125,"occurences":8},"which":{"probability":0.125,"occurences":8},"great":{"probability":0.125,"occurences":8},"particular":{"probability":0.125,"occurences":8},"of":{"probability":0.5,"occurences":32}},"would":{"win":{"probability":0.125,"occurences":8},"go":{"probability":0.125,"occurences":8},"also":{"probability":0.125,"occurences":8},"form":{"probability":0.125,"occurences":8},"coin":{"probability":0.125,"occurences":8},"be":{"probability":0.125,"occurences":8},"present":{"probability":0.125,"occurences":8},"begin.":{"probability":0.125,"occurences":8}},"Internationale":{"Zeitschrift":{"probability":1,"occurences":1}},"year,":{"Freud":{"probability":0.33333333333333,"occurences":3},"his":{"probability":0.33333333333333,"occurences":3},"tasked":{"probability":0.33333333333333,"occurences":3}},"Mathilde":{"(b.":{"probability":1,"occurences":1}},"1913":{"by":{"probability":1,"occurences":1}},"reveal":{"the":{"probability":1,"occurences":1}},"suggestion.":{"The":{"probability":1,"occurences":1}},"Rank":{"and":{"probability":1,"occurences":1}},"gives":{"detailed":{"probability":0.5,"occurences":2},"rise,":{"probability":0.5,"occurences":2}},"affections":{"led":{"probability":1,"occurences":1}},"studies":{"edited":{"probability":1,"occurences":1}},"literary":{"studies":{"probability":1,"occurences":1}},"jealousy":{"over":{"probability":1,"occurences":1}},"delivering":{"to":{"probability":1,"occurences":1}},"cultural":{"and":{"probability":1,"occurences":1}},"work.":{"After":{"probability":0.16666666666667,"occurences":6},"His":{"probability":0.16666666666667,"occurences":6},"He":{"probability":0.16666666666667,"occurences":6},"Their":{"probability":0.16666666666667,"occurences":6},"A":{"probability":0.16666666666667,"occurences":6},"Jones":{"probability":0.16666666666667,"occurences":6}},"point":{"(1874).":{"probability":0.5,"occurences":1},"out":{"probability":0.5,"occurences":1}},"gatherings":{"followed":{"probability":1,"occurences":1}},"devoted":{"to":{"probability":1,"occurences":1}},"else":{"returned":{"probability":1,"occurences":1}},"Imago,":{"a":{"probability":1,"occurences":1}},"no":{"discussed":{"probability":0.33333333333333,"occurences":1},"denied":{"probability":0.33333333333333,"occurences":1},"salary":{"probability":0.33333333333333,"occurences":1}},"accusing":{"him":{"probability":1,"occurences":1}},"fellowship":{"to":{"probability":1,"occurences":1}},"Congress":{"of":{"probability":0.33333333333333,"occurences":3},"with":{"probability":0.33333333333333,"occurences":3},"and":{"probability":0.33333333333333,"occurences":3}},"1911":{"they":{"probability":0.5,"occurences":2},"by":{"probability":0.5,"occurences":2}},"three-year":{"period,":{"probability":1,"occurences":1}},"Breuer's":{"proved":{"probability":1,"occurences":1}},"Psychoanalyse":{"edited":{"probability":1,"occurences":1}},"entered":{"private":{"probability":1,"occurences":1}},"Neues":{"Wiener":{"probability":1,"occurences":1}},"One":{"historian":{"probability":1,"occurences":1}},"monthly":{"Zentralblatt":{"probability":1,"occurences":1}},"perception":{"and":{"probability":1,"occurences":1}},"secular":{"Jewish":{"probability":1,"occurences":1}},"owned":{"and":{"probability":0.5,"occurences":1},"neurologist":{"probability":0.5,"occurences":1}},"still":{"only":{"probability":0.5,"occurences":2},"had":{"probability":0.5,"occurences":2}},"launched":{"in":{"probability":1,"occurences":1}},"Forschungen,":{"was":{"probability":1,"occurences":1}},"studies.":{"During":{"probability":1,"occurences":1}},"foundation":{"of":{"probability":1,"occurences":1}},"subject":{"to":{"probability":0.5,"occurences":2},"of":{"probability":0.5,"occurences":2}},"Psychology":{"was":{"probability":0.33333333333333,"occurences":3},"from":{"probability":0.33333333333333,"occurences":3},"as":{"probability":0.33333333333333,"occurences":3}},"psychoanalytische":{"und":{"probability":1,"occurences":1}},"A":{"journal,":{"probability":1,"occurences":1}},"von":{"Hartmann's":{"probability":0.5,"occurences":2},"Krafft-Ebing.":{"probability":0.5,"occurences":2}},"menstrual":{"pains.":{"probability":1,"occurences":1}},"Bristol,":{"Salzburg":{"probability":1,"occurences":1}},"periods":{"of":{"probability":1,"occurences":1}},"advancing":{"the":{"probability":1,"occurences":1}},"view":{"of":{"probability":0.33333333333333,"occurences":1},"to":{"probability":0.33333333333333,"occurences":1},"in":{"probability":0.33333333333333,"occurences":1}},"retrospectively":{"deemed":{"probability":1,"occurences":1}},"decisions":{"were":{"probability":1,"occurences":1}},"completion":{"of":{"probability":1,"occurences":1}},"social":{"quarter":{"probability":0.5,"occurences":2},"impact":{"probability":0.5,"occurences":2}},"her":{"his":{"probability":0.052631578947368,"occurences":9},"and":{"probability":0.052631578947368,"occurences":9},"in":{"probability":0.052631578947368,"occurences":9},"three":{"probability":0.052631578947368,"occurences":9},"permanently":{"probability":0.052631578947368,"occurences":9},"fiancé.":{"probability":0.052631578947368,"occurences":9},"into":{"probability":0.052631578947368,"occurences":9},"treatment).":{"probability":0.052631578947368,"occurences":9},"of":{"probability":0.052631578947368,"occurences":9},"case":{"probability":0.052631578947368,"occurences":9},"they":{"probability":0.052631578947368,"occurences":9},"studies,":{"probability":0.052631578947368,"occurences":9},"history":{"probability":0.052631578947368,"occurences":9},"illness\"":{"probability":0.052631578947368,"occurences":9},"analysis":{"probability":0.052631578947368,"occurences":9},"symptoms":{"probability":0.10526315789474,"occurences":18},"formally":{"probability":0.052631578947368,"occurences":9},"texts":{"probability":0.052631578947368,"occurences":9}},"major":{"evolutionary":{"probability":1,"occurences":1}},"contemporary":{"theorists":{"probability":1,"occurences":1}},"Important":{"decisions":{"probability":1,"occurences":1}},"as":{"well":{"probability":0.013157894736842,"occurences":28},"isolated":{"probability":0.013157894736842,"occurences":28},"also":{"probability":0.013157894736842,"occurences":28},"she":{"probability":0.013157894736842,"occurences":28},"\"completely":{"probability":0.013157894736842,"occurences":28},"Science":{"probability":0.013157894736842,"occurences":28},"the":{"probability":0.10526315789474,"occurences":224},"later":{"probability":0.013157894736842,"occurences":28},"published.":{"probability":0.013157894736842,"occurences":28},"agreed":{"probability":0.013157894736842,"occurences":28},"by":{"probability":0.013157894736842,"occurences":28},"catalytic":{"probability":0.013157894736842,"occurences":28},"interlocutor.":{"probability":0.013157894736842,"occurences":28},"reluctant":{"probability":0.013157894736842,"occurences":28},"invited":{"probability":0.013157894736842,"occurences":28},"seventeen.":{"probability":0.013157894736842,"occurences":28},"awarded":{"probability":0.013157894736842,"occurences":28},"president,":{"probability":0.013157894736842,"occurences":28},"Eckstein's":{"probability":0.013157894736842,"occurences":28},"curable":{"probability":0.013157894736842,"occurences":28},"convened":{"probability":0.013157894736842,"occurences":28},"Anna":{"probability":0.013157894736842,"occurences":28},"its":{"probability":0.026315789473684,"occurences":56},"using":{"probability":0.013157894736842,"occurences":28},"he":{"probability":0.013157894736842,"occurences":28},"employed":{"probability":0.013157894736842,"occurences":28},"stomach":{"probability":0.013157894736842,"occurences":28},"followed":{"probability":0.013157894736842,"occurences":28},"his":{"probability":0.013157894736842,"occurences":28},"Jones":{"probability":0.013157894736842,"occurences":28},"developed":{"probability":0.013157894736842,"occurences":28},"known":{"probability":0.013157894736842,"occurences":28},"important":{"probability":0.013157894736842,"occurences":28},"retrospectively":{"probability":0.013157894736842,"occurences":28},"called":{"probability":0.013157894736842,"occurences":28},"texts":{"probability":0.013157894736842,"occurences":28},"been":{"probability":0.039473684210526,"occurences":84},"conducting":{"probability":0.013157894736842,"occurences":28},"particularly":{"probability":0.013157894736842,"occurences":28},"of":{"probability":0.013157894736842,"occurences":28},"always":{"probability":0.013157894736842,"occurences":28},"joined":{"probability":0.013157894736842,"occurences":28},"different":{"probability":0.013157894736842,"occurences":28},"published":{"probability":0.013157894736842,"occurences":28},"evidence":{"probability":0.013157894736842,"occurences":28},"one":{"probability":0.013157894736842,"occurences":28},"or":{"probability":0.013157894736842,"occurences":28},"reconstituted":{"probability":0.013157894736842,"occurences":28},"secured":{"probability":0.013157894736842,"occurences":28},"through":{"probability":0.013157894736842,"occurences":28},"restored":{"probability":0.013157894736842,"occurences":28},"Freud's":{"probability":0.013157894736842,"occurences":28},"elected,":{"probability":0.013157894736842,"occurences":28},"repressed":{"probability":0.013157894736842,"occurences":28},"based.":{"probability":0.013157894736842,"occurences":28},"in":{"probability":0.013157894736842,"occurences":28},"launched":{"probability":0.013157894736842,"occurences":28},"a":{"probability":0.10526315789474,"occurences":224},"to":{"probability":0.013157894736842,"occurences":28}},"certain":{"aspects":{"probability":0.5,"occurences":2},"physically":{"probability":0.5,"occurences":2}},"Ferenczi":{"from":{"probability":1,"occurences":1}},"Berlin,":{"Sándor":{"probability":1,"occurences":1}},"mute":{"in":{"probability":1,"occurences":1}},"Karl":{"Abraham":{"probability":1,"occurences":1}},"movement":{"were":{"probability":1,"occurences":1}},"accompanying":{"Freud":{"probability":1,"occurences":1}},"is":{"work.":{"probability":0.0094339622641509,"occurences":2},"in":{"probability":0.0094339622641509,"occurences":2},"on":{"probability":0.0094339622641509,"occurences":2},"first":{"probability":0.018867924528302,"occurences":4},"transition":{"probability":0.0094339622641509,"occurences":2},"can":{"probability":0.0094339622641509,"occurences":2},"adolescent":{"probability":0.0094339622641509,"occurences":2},"time":{"probability":0.0094339622641509,"occurences":2},"herself.":{"probability":0.0094339622641509,"occurences":2},"interest":{"probability":0.0094339622641509,"occurences":2},"secular":{"probability":0.0094339622641509,"occurences":2},"reading":{"probability":0.0094339622641509,"occurences":2},"long-standing":{"probability":0.0094339622641509,"occurences":2},"Psychology":{"probability":0.0094339622641509,"occurences":2},"apartment":{"probability":0.0094339622641509,"occurences":2},"work":{"probability":0.028301886792453,"occurences":6},"Autobiographical":{"probability":0.0094339622641509,"occurences":2},"own":{"probability":0.028301886792453,"occurences":6},"sister-in-law,":{"probability":0.0094339622641509,"occurences":2},"respect.":{"probability":0.0094339622641509,"occurences":2},"friend":{"probability":0.018867924528302,"occurences":4},"theories":{"probability":0.018867924528302,"occurences":4},"friend,":{"probability":0.0094339622641509,"occurences":2},"medical":{"probability":0.018867924528302,"occurences":4},"stay":{"probability":0.0094339622641509,"occurences":2},"nose":{"probability":0.0094339622641509,"occurences":2},"life,":{"probability":0.0094339622641509,"occurences":2},"successful":{"probability":0.0094339622641509,"occurences":2},"procedure,":{"probability":0.0094339622641509,"occurences":2},"with":{"probability":0.0094339622641509,"occurences":2},"new":{"probability":0.0094339622641509,"occurences":2},"loyalty":{"probability":0.0094339622641509,"occurences":2},"patients'":{"probability":0.0094339622641509,"occurences":2},"psychoanalytic":{"probability":0.0094339622641509,"occurences":2},"group":{"probability":0.0094339622641509,"occurences":2},"\"specifically":{"probability":0.0094339622641509,"occurences":2},"(she":{"probability":0.0094339622641509,"occurences":2},"philosophy":{"probability":0.0094339622641509,"occurences":2},"discussion":{"probability":0.018867924528302,"occurences":4},"family":{"probability":0.0094339622641509,"occurences":2},"work,":{"probability":0.0094339622641509,"occurences":2},"correspondence":{"probability":0.0094339622641509,"occurences":2},"disastrous":{"probability":0.0094339622641509,"occurences":2},"was":{"probability":0.0094339622641509,"occurences":2},"letters":{"probability":0.0094339622641509,"occurences":2},"research":{"probability":0.0094339622641509,"occurences":2},"Project":{"probability":0.0094339622641509,"occurences":2},"to":{"probability":0.0094339622641509,"occurences":2},"patient":{"probability":0.0094339622641509,"occurences":2},"understanding":{"probability":0.0094339622641509,"occurences":2},"appointment":{"probability":0.018867924528302,"occurences":4},"intellectual":{"probability":0.018867924528302,"occurences":4},"English":{"probability":0.0094339622641509,"occurences":2},"explorations":{"probability":0.0094339622641509,"occurences":2},"theoretical":{"probability":0.0094339622641509,"occurences":2},"hospital":{"probability":0.0094339622641509,"occurences":2},"early":{"probability":0.0094339622641509,"occurences":2},"mother's":{"probability":0.0094339622641509,"occurences":2},"account":{"probability":0.0094339622641509,"occurences":2},"father":{"probability":0.018867924528302,"occurences":4},"general":{"probability":0.0094339622641509,"occurences":2},"based.":{"probability":0.0094339622641509,"occurences":2},"formative":{"probability":0.0094339622641509,"occurences":2},"prestige":{"probability":0.0094339622641509,"occurences":2},"clinical":{"probability":0.028301886792453,"occurences":6},"way,":{"probability":0.0094339622641509,"occurences":2},"of":{"probability":0.028301886792453,"occurences":6},"consequent":{"probability":0.0094339622641509,"occurences":2},"allegiance":{"probability":0.0094339622641509,"occurences":2},"meeting,":{"probability":0.0094339622641509,"occurences":2},"Saturday":{"probability":0.0094339622641509,"occurences":2},"collected":{"probability":0.0094339622641509,"occurences":2},"for":{"probability":0.0094339622641509,"occurences":2},"conversion":{"probability":0.0094339622641509,"occurences":2},"Jewish":{"probability":0.0094339622641509,"occurences":2},"Three":{"probability":0.0094339622641509,"occurences":2},"capacity":{"probability":0.0094339622641509,"occurences":2},"feelings":{"probability":0.0094339622641509,"occurences":2},"is":{"probability":0.0094339622641509,"occurences":2},"country":{"probability":0.0094339622641509,"occurences":2},"substantial":{"probability":0.0094339622641509,"occurences":2},"theory":{"probability":0.028301886792453,"occurences":6},"ambition":{"probability":0.0094339622641509,"occurences":2},"which":{"probability":0.0094339622641509,"occurences":2},"more":{"probability":0.018867924528302,"occurences":4},"increasingly":{"probability":0.0094339622641509,"occurences":2},"self-analysis,":{"probability":0.0094339622641509,"occurences":2}},"contingents":{"accompanying":{"probability":1,"occurences":1}},"silence":{"on":{"probability":1,"occurences":1}},"\"forty-two":{"present,":{"probability":1,"occurences":1}},"records,":{"\"forty-two":{"probability":1,"occurences":1}},"physicians":{"who":{"probability":0.5,"occurences":2},"and":{"probability":0.5,"occurences":2}},"Congress.":{"There":{"probability":1,"occurences":1}},"psychology":{"with":{"probability":0.25,"occurences":3},"were":{"probability":0.25,"occurences":3},"and":{"probability":0.25,"occurences":3},"may":{"probability":0.25,"occurences":3}},"arguing":{"that":{"probability":1,"occurences":1}},"Jones":{"organised":{"probability":0.2,"occurences":5},"to":{"probability":0.2,"occurences":5},"had":{"probability":0.2,"occurences":5},"records,":{"probability":0.2,"occurences":5},"and":{"probability":0.2,"occurences":5}},"applying":{"psychoanalytic":{"probability":1,"occurences":1}},"London-based":{"neurologist":{"probability":1,"occurences":1}},"Ernest":{"Jones,":{"probability":1,"occurences":1}},"convened":{"at":{"probability":1,"occurences":1}},"Once":{"he":{"probability":1,"occurences":1}},"Science":{"arguably":{"probability":1,"occurences":1}},"deemed":{"to":{"probability":1,"occurences":1}},"association\",":{"Freud":{"probability":1,"occurences":1}},"depression,":{"a":{"probability":1,"occurences":1}},"[Freud's]":{"affection\".":{"probability":1,"occurences":1}},"formally":{"for":{"probability":1,"occurences":1}},"suicide.":{"Reitler":{"probability":1,"occurences":1}},"became":{"pathogenic":{"probability":0.14285714285714,"occurences":7},"one":{"probability":0.14285714285714,"occurences":7},"the":{"probability":0.14285714285714,"occurences":7},"practicing":{"probability":0.14285714285714,"occurences":7},"a":{"probability":0.28571428571429,"occurences":14},"reduced":{"probability":0.14285714285714,"occurences":7}},"together":{"formally":{"probability":1,"occurences":1}},"being":{"no":{"probability":1,"occurences":1}},"followers":{"met":{"probability":1,"occurences":1}},"attached":{"to":{"probability":1,"occurences":1}},"habitual":{"masturbation":{"probability":1,"occurences":1}},"structure":{"(the":{"probability":1,"occurences":1}},"evolution":{"of":{"probability":1,"occurences":1}},"significance":{"in":{"probability":1,"occurences":1}},"resulting":{"in":{"probability":1,"occurences":1}},"turning":{"him":{"probability":1,"occurences":1}},"\"actual":{"neuroses,\"":{"probability":1,"occurences":1}},"hospital.":{"His":{"probability":1,"occurences":1}},"psychiatrists":{"and":{"probability":1,"occurences":1}},"Spielrein":{"who":{"probability":0.5,"occurences":2},"had":{"probability":0.5,"occurences":2}},"accompanied":{"by":{"probability":1,"occurences":1}},"Rosenthal":{"and":{"probability":1,"occurences":1}},"investigation":{"appear":{"probability":1,"occurences":1}},"standpoint.":{"The":{"probability":1,"occurences":1}},"neutralizing":{"his":{"probability":1,"occurences":1}},"promising":{"career":{"probability":1,"occurences":1}},"suffering":{"a":{"probability":1,"occurences":1}},"Adler":{"in":{"probability":0.5,"occurences":2},"and":{"probability":0.5,"occurences":2}},"initial":{"inception,":{"probability":1,"occurences":1}},"favor":{"of":{"probability":1,"occurences":1}},"talk":{"about":{"probability":0.5,"occurences":2},"freely,":{"probability":0.5,"occurences":2}},"lecture":{"hall":{"probability":1,"occurences":1}},"fled":{"from":{"probability":1,"occurences":1}},"(1869).":{"Other":{"probability":1,"occurences":1}},"anxiety":{"symptoms.":{"probability":1,"occurences":1}},"Psychoanalytic":{"Congress,":{"probability":0.2,"occurences":5},"Association":{"probability":0.2,"occurences":5},"Society":{"probability":0.6,"occurences":15}},"status,":{"the":{"probability":1,"occurences":1}},"growing":{"institutional":{"probability":1,"occurences":1}},"always":{"spoken":{"probability":1,"occurences":1}},"reflecting":{"its":{"probability":1,"occurences":1}},"unconscious,":{"pre-conscious":{"probability":1,"occurences":1}},"1908,":{"reflecting":{"probability":1,"occurences":1}},"Thereafter,":{"they":{"probability":1,"occurences":1}},"nose":{"and":{"probability":1,"occurences":9}},"based":{"on":{"probability":0.25,"occurences":1},"Abraham":{"probability":0.25,"occurences":1},"ear,":{"probability":0.25,"occurences":1},"neurologist":{"probability":0.25,"occurences":1}},"visit":{"Freud":{"probability":0.5,"occurences":2},"to":{"probability":0.5,"occurences":2}},"\"the":{"bounding":{"probability":0.25,"occurences":4},"words":{"probability":0.25,"occurences":4},"one":{"probability":0.25,"occurences":4},"Kepler":{"probability":0.25,"occurences":4}},"Ludwig":{"Binswanger,":{"probability":1,"occurences":1}},"1907,":{"Jung":{"probability":1,"occurences":1}},"States,":{"accompanied":{"probability":1,"occurences":1}},"attracted":{"widespread":{"probability":1,"occurences":1}},"Mental":{"Hospital":{"probability":1,"occurences":1}},"Eugen":{"Bleuler":{"probability":1,"occurences":1}},"assistant":{"to":{"probability":1,"occurences":1}},"friendship":{"came":{"probability":1,"occurences":1}},"until":{"they":{"probability":0.5,"occurences":2},"late":{"probability":0.5,"occurences":2}},"Skin":{"Response,":{"probability":1,"occurences":1}},"(co-authored":{"with":{"probability":1,"occurences":1}},"researcher":{"into":{"probability":1,"occurences":1}},"Physicians,":{"was":{"probability":1,"occurences":1}},"members,":{"including":{"probability":1,"occurences":1}},"discuss":{"issues":{"probability":1,"occurences":1}},"failed":{"to":{"probability":1,"occurences":1}},"toward":{"the":{"probability":1,"occurences":1}},"group's":{"paid":{"probability":1,"occurences":1}},"Rank,":{"who":{"probability":1,"occurences":1}},"complex":{"structuring":{"probability":1,"occurences":1}},"these":{"new":{"probability":0.5,"occurences":2},"were":{"probability":0.5,"occurences":2}},"adolescent":{"friend":{"probability":0.5,"occurences":2},"self-cutting":{"probability":0.5,"occurences":2}},"offer":{"of":{"probability":1,"occurences":1}},"Darwin's":{"major":{"probability":1,"occurences":1}},"Gustav":{"Jung":{"probability":1,"occurences":1}},"Oliver":{"(b.":{"probability":1,"occurences":1}},"Jung,":{"of":{"probability":0.5,"occurences":2},"also":{"probability":0.5,"occurences":2}},"grown":{"to":{"probability":1,"occurences":1}},"Hilferding,":{"joined":{"probability":1,"occurences":1}},"prophet":{"who":{"probability":1,"occurences":1}},"religion":{"in":{"probability":1,"occurences":1}},"William":{"Shakespeare":{"probability":1,"occurences":1}},"Dorotheergasse":{"which":{"probability":1,"occurences":1}},"all":{"the":{"probability":0.16666666666667,"occurences":2},"psychoanalytic":{"probability":0.16666666666667,"occurences":2},"of":{"probability":0.16666666666667,"occurences":2},"audiences":{"probability":0.16666666666667,"occurences":2},"five":{"probability":0.16666666666667,"occurences":2},"neuroses":{"probability":0.16666666666667,"occurences":2}},"outline":{"of":{"probability":1,"occurences":1}},"hour,":{"the":{"probability":1,"occurences":1}},"obsessional":{"neurosis),":{"probability":1,"occurences":1}},"served;":{"cigar":{"probability":1,"occurences":1}},"non-salaried":{"post":{"probability":1,"occurences":1}},"concluded":{"that":{"probability":1,"occurences":9}},"cakes":{"were":{"probability":1,"occurences":1}},"coffee":{"and":{"probability":1,"occurences":1}},"These":{"pains":{"probability":1,"occurences":1}},"words":{"for":{"probability":1,"occurences":1}},"who":{"were":{"probability":0.0625,"occurences":16},"made":{"probability":0.0625,"occurences":16},"(supposedly)":{"probability":0.0625,"occurences":16},"invited":{"probability":0.0625,"occurences":16},"in":{"probability":0.0625,"occurences":16},"had":{"probability":0.25,"occurences":64},"was":{"probability":0.375,"occurences":96},"first":{"probability":0.0625,"occurences":16}},"Then,":{"black":{"probability":1,"occurences":1}},"teaching":{"duties":{"probability":1,"occurences":1}},"triggered":{"as":{"probability":1,"occurences":1}},"Freud.":{"Kahane":{"probability":0.33333333333333,"occurences":3},"She":{"probability":0.33333333333333,"occurences":3},"They":{"probability":0.33333333333333,"occurences":3}},"providing":{"thermal":{"probability":1,"occurences":1}},"Stekel.":{"Stekel":{"probability":1,"occurences":1}},"ritual.":{"First":{"probability":1,"occurences":1}},"followed":{"a":{"probability":0.5,"occurences":2},"in":{"probability":0.5,"occurences":2}},"me.\"":{"Later,":{"probability":1,"occurences":1}},"application":{"of":{"probability":1,"occurences":1}},"\"completely":{"without":{"probability":1,"occurences":1}},"series":{"of":{"probability":1,"occurences":1}},"atmosphere":{"of":{"probability":1,"occurences":4}},"caused":{"by":{"probability":1,"occurences":1}},"decided":{"on":{"probability":1,"occurences":1}},"–":{"the":{"probability":0.16666666666667,"occurences":6},"he":{"probability":0.33333333333333,"occurences":12},"masturbation,":{"probability":0.16666666666667,"occurences":6},"in":{"probability":0.16666666666667,"occurences":6},"Freud":{"probability":0.16666666666667,"occurences":6}},"soon":{"after":{"probability":1,"occurences":1}},"are":{"in":{"probability":0.33333333333333,"occurences":1},"today":{"probability":0.33333333333333,"occurences":1},"of":{"probability":0.33333333333333,"occurences":1}},"encountered":{"Freud":{"probability":1,"occurences":1}},"Hans\",":{"who":{"probability":1,"occurences":1}},"respond":{"to":{"probability":1,"occurences":1}},"Vienna":{"following":{"probability":0.125,"occurences":8},"Psychoanalytic":{"probability":0.125,"occurences":8},"in":{"probability":0.25,"occurences":16},"General":{"probability":0.125,"occurences":8},"to":{"probability":0.125,"occurences":8},"University,":{"probability":0.125,"occurences":8},"under":{"probability":0.125,"occurences":8}},"\"Little":{"Hans\",":{"probability":1,"occurences":1}},"musicologist":{"and":{"probability":1,"occurences":1}},"forms":{"and":{"probability":1,"occurences":1}},"Graf,":{"a":{"probability":1,"occurences":1}},"life,":{"and":{"probability":1,"occurences":1}},"psychopathologishe":{"Forschungen,":{"probability":1,"occurences":1}},"interested":{"in":{"probability":1,"occurences":1}},"manual":{"for":{"probability":1,"occurences":1}},"socialist":{"who":{"probability":1,"occurences":1}},"circle,":{"was":{"probability":1,"occurences":1}},"habit.\"":{"Freud":{"probability":1,"occurences":1}},"prompted":{"a":{"probability":1,"occurences":1}},"among":{"the":{"probability":1,"occurences":1}},"attention":{"has":{"probability":1,"occurences":1}},"acrimonious":{"end":{"probability":1,"occurences":1}},"removal":{"of":{"probability":1,"occurences":4}},"most":{"formidable":{"probability":1,"occurences":1}},"regarded":{"as":{"probability":1,"occurences":1}},"role":{"and":{"probability":1,"occurences":1}},"1917.":{"Adler,":{"probability":1,"occurences":1}},"word":{"was":{"probability":1,"occurences":1}},"appointment":{"as":{"probability":0.5,"occurences":2},"had":{"probability":0.5,"occurences":2}},"thermal":{"cures":{"probability":1,"occurences":1}},"establishment":{"providing":{"probability":1,"occurences":1}},"Swiss":{"hotel":{"probability":0.5,"occurences":2},"psychiatrist,":{"probability":0.5,"occurences":2}},"resigned":{"his":{"probability":1,"occurences":1}},"Nuremberg":{"Congress":{"probability":1,"occurences":1}},"friends":{"of":{"probability":1,"occurences":1}},"unknown":{"reasons":{"probability":1,"occurences":1}},"method.":{"Kahane":{"probability":1,"occurences":1}},"begin.":{"The":{"probability":1,"occurences":1}},"successive":{"years":{"probability":1,"occurences":1}},"Fliess":{"angry":{"probability":0.1,"occurences":10},"repeatedly":{"probability":0.1,"occurences":10},"developed":{"probability":0.1,"occurences":10},"was":{"probability":0.1,"occurences":10},"failed":{"probability":0.1,"occurences":10},"the":{"probability":0.1,"occurences":10},"reveal,":{"probability":0.1,"occurences":10},"in":{"probability":0.1,"occurences":10},"as":{"probability":0.1,"occurences":10},"\"the":{"probability":0.1,"occurences":10}},"published.":{"In":{"probability":1,"occurences":1}},"critical":{"review":{"probability":0.5,"occurences":2},"standpoint.":{"probability":0.5,"occurences":2}},"three-month":{"fellowship":{"probability":1,"occurences":1}},"formation":{"of":{"probability":1,"occurences":4}},"Medicine":{"for":{"probability":1,"occurences":1}},"Internal":{"Medicine":{"probability":1,"occurences":1}},"Outline":{"of":{"probability":1,"occurences":1}},"infantile":{"sexual":{"probability":0.5,"occurences":18},"sexuality":{"probability":0.33333333333333,"occurences":12},"sexuality,":{"probability":0.16666666666667,"occurences":6}},"mother's":{"affections":{"probability":1,"occurences":1}},"1901,":{"Kahane,":{"probability":1,"occurences":1}},"variously":{"attributed":{"probability":1,"occurences":1}},"years":{"by":{"probability":1,"occurences":1}},"1909":{"under":{"probability":0.5,"occurences":2},"at":{"probability":0.5,"occurences":2}},"attendance":{"at":{"probability":1,"occurences":1}},"into":{"hypnosis.":{"probability":0.5,"occurences":2},"word-association":{"probability":0.5,"occurences":2}},"through":{"their":{"probability":0.5,"occurences":1},"for":{"probability":0.5,"occurences":1}},"less":{"continued":{"probability":0.5,"occurences":1},"financially":{"probability":0.5,"occurences":1}},"abreast":{"of":{"probability":1,"occurences":1}},"kept":{"abreast":{"probability":1,"occurences":1}},"school":{"and":{"probability":1,"occurences":1}},"including":{"that":{"probability":0.5,"occurences":2},"Otto":{"probability":0.5,"occurences":2}},"secondary":{"school":{"probability":1,"occurences":1}},"attended":{"the":{"probability":1,"occurences":1}},"1923":{"committed":{"probability":1,"occurences":1}},"Kahane":{"broke":{"probability":0.33333333333333,"occurences":3},"had":{"probability":0.33333333333333,"occurences":3},"and":{"probability":0.33333333333333,"occurences":3}},"five":{"were":{"probability":0.5,"occurences":2},"lectures":{"probability":0.5,"occurences":2}},"result":{"of":{"probability":1,"occurences":1}},"Reitler,":{"were":{"probability":1,"occurences":1}},"reduced":{"in":{"probability":1,"occurences":1}},"bleeding":{"–":{"probability":1,"occurences":1}},"asylum":{"led":{"probability":1,"occurences":1}},"Max":{"Graf,":{"probability":0.33333333333333,"occurences":3},"Eitingon":{"probability":0.33333333333333,"occurences":3},"Kahane,":{"probability":0.33333333333333,"occurences":3}},"Alfred":{"Adler,":{"probability":1,"occurences":1}},"attend,":{"Alfred":{"probability":1,"occurences":1}},"perverse\"":{"forms":{"probability":1,"occurences":1}},"original":{"members":{"probability":1,"occurences":1}},"turbinate.":{"Fliess's":{"probability":1,"occurences":1}},"systematic":{"theory":{"probability":1,"occurences":1}},"three":{"original":{"probability":1,"occurences":1}},"Zentralblatt":{"für":{"probability":1,"occurences":1}},"newspaper":{"Neues":{"probability":1,"occurences":1}},"neurologist":{"who":{"probability":0.66666666666667,"occurences":6},"and":{"probability":0.33333333333333,"occurences":3}},"memories":{"of":{"probability":0.75,"occurences":12},"occurred":{"probability":0.25,"occurences":4}},"using":{"the":{"probability":0.25,"occurences":2},"[Freud's]":{"probability":0.25,"occurences":2},"him":{"probability":0.25,"occurences":2},"hypnosis":{"probability":0.25,"occurences":2}},"neuropathology":{"in":{"probability":1,"occurences":1}},"relief":{"could":{"probability":1,"occurences":1}},"Eckstein":{"to":{"probability":0.5,"occurences":2},"nonetheless":{"probability":0.5,"occurences":2}},"secretary.":{"In":{"probability":1,"occurences":1}},"himself":{"to":{"probability":0.5,"occurences":2},"was":{"probability":0.5,"occurences":2}},"take":{"up":{"probability":1,"occurences":1}},"inconsistent":{"results":{"probability":1,"occurences":1}},"Students":{"and":{"probability":1,"occurences":1}},"collected":{"works;":{"probability":1,"occurences":1}},"out":{"the":{"probability":0.125,"occurences":3},"his":{"probability":0.25,"occurences":6},"her":{"probability":0.125,"occurences":3},"in":{"probability":0.125,"occurences":3},"censorship":{"probability":0.125,"occurences":3},"blame\",":{"probability":0.125,"occurences":3},"whatever":{"probability":0.125,"occurences":3}},"1892),":{"Sophie":{"probability":1,"occurences":1}},"linked,":{"was":{"probability":1,"occurences":1}},"extensive":{"correspondence":{"probability":0.66666666666667,"occurences":6},"discussions":{"probability":0.33333333333333,"occurences":3}},"works,":{"and":{"probability":1,"occurences":1}},"admired":{"his":{"probability":1,"occurences":1}},"cancer.":{"Freud":{"probability":1,"occurences":1}},"back":{"in":{"probability":0.5,"occurences":2},"to":{"probability":0.5,"occurences":2}},"This":{"was":{"probability":0.25,"occurences":4},"transition":{"probability":0.25,"occurences":4},"group":{"probability":0.25,"occurences":4},"meeting,":{"probability":0.25,"occurences":4}},"advocacy":{"prepared":{"probability":1,"occurences":1}},"committed":{"suicide.":{"probability":1,"occurences":1}},"moral":{"outlook,":{"probability":1,"occurences":1}},"journal":{"devoted":{"probability":1,"occurences":1}},"important":{"to":{"probability":0.5,"occurences":2},"contributions":{"probability":0.5,"occurences":2}},"family":{"lived":{"probability":1,"occurences":1}},"concluded,":{"underlay":{"probability":0.5,"occurences":2},"based":{"probability":0.5,"occurences":2}},"age":{"in":{"probability":0.5,"occurences":1},"24;":{"probability":0.5,"occurences":1}},"loved":{"in":{"probability":1,"occurences":1}},"associated":{"with":{"probability":1,"occurences":1}},"following":{"the":{"probability":0.33333333333333,"occurences":3},"year":{"probability":0.33333333333333,"occurences":3},"a":{"probability":0.33333333333333,"occurences":3}},"find":{"in":{"probability":1,"occurences":1}},"Autobiographical":{"Study.":{"probability":1,"occurences":1}},"could":{"exercise":{"probability":0.25,"occurences":4},"only":{"probability":0.25,"occurences":4},"be":{"probability":0.5,"occurences":8}},"experience":{"of":{"probability":1,"occurences":1}},"menstrual)":{"bleeding,":{"probability":1,"occurences":1}},"having":{"concluded":{"probability":1,"occurences":1}},"children:":{"Mathilde":{"probability":1,"occurences":1}},"conducting":{"scientific":{"probability":1,"occurences":1}},"York":{"Psychoanalytic":{"probability":1,"occurences":1}},"increased":{"interest":{"probability":1,"occurences":1}},"that":{"infantile":{"probability":0.052631578947368,"occurences":19},"to":{"probability":0.052631578947368,"occurences":19},"in":{"probability":0.052631578947368,"occurences":19},"patients'":{"probability":0.052631578947368,"occurences":19},"Freud":{"probability":0.052631578947368,"occurences":19},"he":{"probability":0.10526315789474,"occurences":38},"more":{"probability":0.052631578947368,"occurences":19},"room.":{"probability":0.052631578947368,"occurences":19},"every":{"probability":0.052631578947368,"occurences":19},"presupposes":{"probability":0.052631578947368,"occurences":19},"it":{"probability":0.052631578947368,"occurences":19},"Fliess":{"probability":0.052631578947368,"occurences":19},"his":{"probability":0.052631578947368,"occurences":19},"of":{"probability":0.052631578947368,"occurences":19},"addictions,":{"probability":0.052631578947368,"occurences":19},"remains":{"probability":0.052631578947368,"occurences":19},"a":{"probability":0.052631578947368,"occurences":19},"unconscious":{"probability":0.052631578947368,"occurences":19}},"intimate":{"in":{"probability":1,"occurences":1}},"neurasthenia":{"and":{"probability":1,"occurences":1}},"connection":{"which":{"probability":1,"occurences":1}},"studied.\"":{"His":{"probability":1,"occurences":1}},"aphasia":{"would":{"probability":1,"occurences":1}},"genitalia":{"were":{"probability":1,"occurences":1}},"specializing":{"in":{"probability":1,"occurences":1}},"disastrous,":{"resulting":{"probability":1,"occurences":1}},"president":{"of":{"probability":0.5,"occurences":2},"and":{"probability":0.5,"occurences":2}},"works":{"which":{"probability":0.5,"occurences":2},"\"the":{"probability":0.5,"occurences":2}},"make":{"important":{"probability":1,"occurences":1}},"career":{"in":{"probability":0.66666666666667,"occurences":6},"at":{"probability":0.33333333333333,"occurences":3}},"experienced":{"heart":{"probability":1,"occurences":1}},"procedure,":{"which":{"probability":1,"occurences":1}},"much":{"that":{"probability":1,"occurences":1}},"practice.":{"Described":{"probability":1,"occurences":1}},"self-control":{"in":{"probability":1,"occurences":1}},"pseudoscientific.":{"He":{"probability":1,"occurences":1}},"hotel":{"guest-book":{"probability":1,"occurences":1}},"psychopathology":{"and":{"probability":1,"occurences":1}},"remedial":{"surgery":{"probability":1,"occurences":1}},"period":{"in":{"probability":0.33333333333333,"occurences":3},"of":{"probability":0.66666666666667,"occurences":6}},"health":{"warnings":{"probability":0.5,"occurences":2},"manual":{"probability":0.5,"occurences":2}},"From":{"the":{"probability":0.5,"occurences":2},"1891":{"probability":0.5,"occurences":2}},"According":{"to":{"probability":1,"occurences":1}},"bounding":{"of":{"probability":1,"occurences":1}},"evening":{"lectures.":{"probability":0.5,"occurences":2},"at":{"probability":0.5,"occurences":2}},"men":{"saw":{"probability":0.5,"occurences":1},"would":{"probability":0.5,"occurences":1}},"evolutionary":{"writings,":{"probability":1,"occurences":1}},"group":{"soon":{"probability":0.16666666666667,"occurences":6},"in":{"probability":0.16666666666667,"occurences":6},"had":{"probability":0.16666666666667,"occurences":6},"was":{"probability":0.33333333333333,"occurences":12},"at":{"probability":0.16666666666667,"occurences":6}},"more":{"influential":{"probability":0.2,"occurences":5},"consistent":{"probability":0.2,"occurences":5},"general":{"probability":0.2,"occurences":5},"than":{"probability":0.2,"occurences":5},"famous":{"probability":0.2,"occurences":5}},"spent":{"in":{"probability":1,"occurences":1}},"not":{"use":{"probability":0.25,"occurences":4},"yet":{"probability":0.25,"occurences":4},"to":{"probability":0.25,"occurences":4},"matter":{"probability":0.25,"occurences":4}},"concepts":{"of":{"probability":1,"occurences":1}},"restricted":{"mobility,":{"probability":1,"occurences":1}},"influence":{"in":{"probability":1,"occurences":1}},"operate":{"on":{"probability":1,"occurences":1}},"though":{"aware":{"probability":0.25,"occurences":2},"Brentano":{"probability":0.25,"occurences":2},"some":{"probability":0.25,"occurences":2},"still":{"probability":0.25,"occurences":2}},"psychiatry.":{"Max":{"probability":1,"occurences":1}},"departments":{"of":{"probability":1,"occurences":1}},"\"nervous":{"disorders\".":{"probability":1,"occurences":1}},"cures":{"in":{"probability":1,"occurences":1}},"studies,":{"Spielrein":{"probability":1,"occurences":1}},"were,":{"according":{"probability":0.5,"occurences":2},"as":{"probability":0.5,"occurences":2}},"\"professor":{"extraordinarius\"":{"probability":0.5,"occurences":2},"ordinarius\"":{"probability":0.5,"occurences":2}},"maintained":{"an":{"probability":0.5,"occurences":2},"a":{"probability":0.5,"occurences":2}},"to":{"visit":{"probability":0.0095238095238095,"occurences":101},"hypnosis.":{"probability":0.0095238095238095,"occurences":101},"endorse":{"probability":0.0095238095238095,"occurences":101},"later":{"probability":0.0095238095238095,"occurences":101},"take":{"probability":0.0095238095238095,"occurences":101},"an":{"probability":0.028571428571429,"occurences":303},"refer":{"probability":0.0095238095238095,"occurences":101},"study":{"probability":0.0095238095238095,"occurences":101},"Brill":{"probability":0.0095238095238095,"occurences":101},"word-association":{"probability":0.0095238095238095,"occurences":101},"full":{"probability":0.0095238095238095,"occurences":101},"delicately":{"probability":0.0095238095238095,"occurences":101},"find":{"probability":0.0095238095238095,"occurences":101},"Jones":{"probability":0.0095238095238095,"occurences":101},"work":{"probability":0.0095238095238095,"occurences":101},"Freud's":{"probability":0.028571428571429,"occurences":303},"\"an":{"probability":0.0095238095238095,"occurences":101},"hypnosis,":{"probability":0.0095238095238095,"occurences":101},"build":{"probability":0.0095238095238095,"occurences":101},"tobacco,":{"probability":0.0095238095238095,"occurences":101},"bribe":{"probability":0.0095238095238095,"occurences":101},"respond":{"probability":0.0095238095238095,"occurences":101},"rumours,":{"probability":0.0095238095238095,"occurences":101},"give":{"probability":0.0095238095238095,"occurences":101},"psychology":{"probability":0.0095238095238095,"occurences":101},"university":{"probability":0.0095238095238095,"occurences":101},"Paris":{"probability":0.0095238095238095,"occurences":101},"have":{"probability":0.0095238095238095,"occurences":101},"demonstrate":{"probability":0.0095238095238095,"occurences":101},"elaborate":{"probability":0.0095238095238095,"occurences":101},"psychoanalysis":{"probability":0.0095238095238095,"occurences":101},"Freud":{"probability":0.019047619047619,"occurences":202},"develop":{"probability":0.0095238095238095,"occurences":101},"point":{"probability":0.0095238095238095,"occurences":101},"meet":{"probability":0.0095238095238095,"occurences":101},"practice":{"probability":0.0095238095238095,"occurences":101},"Fliess":{"probability":0.028571428571429,"occurences":303},"spread":{"probability":0.0095238095238095,"occurences":101},"Fliess's":{"probability":0.0095238095238095,"occurences":101},"attend,":{"probability":0.0095238095238095,"occurences":101},"his":{"probability":0.076190476190476,"occurences":808},"be":{"probability":0.076190476190476,"occurences":808},"talk":{"probability":0.019047619047619,"occurences":202},"the":{"probability":0.12380952380952,"occurences":1313},"discuss":{"probability":0.0095238095238095,"occurences":101},"Vienna":{"probability":0.019047619047619,"occurences":202},"abandon":{"probability":0.0095238095238095,"occurences":101},"appear":{"probability":0.0095238095238095,"occurences":101},"associate":{"probability":0.0095238095238095,"occurences":101},"further":{"probability":0.0095238095238095,"occurences":101},"Eugen":{"probability":0.0095238095238095,"occurences":101},"rely":{"probability":0.0095238095238095,"occurences":101},"analogies":{"probability":0.0095238095238095,"occurences":101},"make":{"probability":0.0095238095238095,"occurences":101},"one":{"probability":0.0095238095238095,"occurences":101},"Rank,":{"probability":0.0095238095238095,"occurences":101},"advancing":{"probability":0.0095238095238095,"occurences":101},"them.":{"probability":0.0095238095238095,"occurences":101},"treat":{"probability":0.019047619047619,"occurences":202},"small":{"probability":0.0095238095238095,"occurences":101},"reveal":{"probability":0.0095238095238095,"occurences":101},"fundamentally":{"probability":0.0095238095238095,"occurences":101},"Freud,":{"probability":0.0095238095238095,"occurences":101},"sixteen":{"probability":0.0095238095238095,"occurences":101},"which":{"probability":0.019047619047619,"occurences":202},"him.":{"probability":0.0095238095238095,"occurences":101},"organize":{"probability":0.0095238095238095,"occurences":101},"recall":{"probability":0.0095238095238095,"occurences":101}},"Philosophy":{"of":{"probability":1,"occurences":1}},"neurosis":{"can":{"probability":1,"occurences":1}},"under":{"the":{"probability":0.33333333333333,"occurences":3},"Richard":{"probability":0.33333333333333,"occurences":3},"hypnosis":{"probability":0.33333333333333,"occurences":3}},"different":{"from":{"probability":1,"occurences":1}},"revise":{"his":{"probability":1,"occurences":4}},"tutor,":{"Brentano,":{"probability":1,"occurences":1}},"account":{"is":{"probability":1,"occurences":1}},"known":{"for":{"probability":0.33333333333333,"occurences":2},"as":{"probability":0.33333333333333,"occurences":2},"reasons":{"probability":0.33333333333333,"occurences":2}},"1900":{"and":{"probability":1,"occurences":1}},"bought":{"his":{"probability":1,"occurences":1}},"fiancé.":{"The":{"probability":1,"occurences":1}},"Psychopathology":{"of":{"probability":1,"occurences":1}},"treatment).":{"In":{"probability":1,"occurences":1}},"Response,":{"and":{"probability":1,"occurences":1}},"influential":{"ex-patients,":{"probability":0.5,"occurences":2},"paper":{"probability":0.5,"occurences":2}},"Life":{"(1901)":{"probability":1,"occurences":1}},"By":{"1896":{"probability":0.5,"occurences":2},"1906,":{"probability":0.5,"occurences":2}},"Both":{"were":{"probability":0.25,"occurences":4},"Kahane":{"probability":0.25,"occurences":4},"men":{"probability":0.25,"occurences":4},"women":{"probability":0.25,"occurences":4}},"editorship":{"of":{"probability":1,"occurences":1}},"1900,":{"the":{"probability":1,"occurences":1}},"rabbi":{"in":{"probability":1,"occurences":1}},"1887),":{"Jean-Martin":{"probability":1,"occurences":1}},"neurology":{"and":{"probability":0.5,"occurences":2},"research.":{"probability":0.5,"occurences":2}},"helped":{"introduce":{"probability":1,"occurences":1}},"greatly":{"admired":{"probability":1,"occurences":1}},"disfigured.":{"At":{"probability":1,"occurences":1}},"During":{"this":{"probability":1,"occurences":1}},"transformative":{"for":{"probability":1,"occurences":1}},"probably":{"helped":{"probability":1,"occurences":1}},"collaboration":{"over":{"probability":1,"occurences":1}},"Untimely":{"Meditations":{"probability":1,"occurences":1}},"collaborator,":{"Josef":{"probability":1,"occurences":1}},"Meditations":{"when":{"probability":1,"occurences":1}},"introduce":{"Freud":{"probability":1,"occurences":1}},"lecturer":{"or":{"probability":0.5,"occurences":2},"at":{"probability":0.5,"occurences":2}},"read":{"the":{"probability":0.2,"occurences":3},"until":{"probability":0.2,"occurences":3},"William":{"probability":0.2,"occurences":3},"Nietzsche's":{"probability":0.2,"occurences":3},"media":{"probability":0.2,"occurences":3}},"work\".":{"He":{"probability":1,"occurences":1}},"went":{"to":{"probability":0.66666666666667,"occurences":6},"on":{"probability":0.33333333333333,"occurences":3}},"Jones's":{"advocacy":{"probability":1,"occurences":1}},"blocked":{"in":{"probability":1,"occurences":1}},"introspection.":{"Brentano":{"probability":1,"occurences":1}},"detailed":{"interpretations":{"probability":1,"occurences":1}},"postulated":{"that":{"probability":1,"occurences":1}},"far":{"more":{"probability":1,"occurences":1}},"Abraham":{"and":{"probability":0.5,"occurences":2},"Brill.":{"probability":0.5,"occurences":2}},"1897":{"that":{"probability":1,"occurences":1}},"Fragment":{"of":{"probability":1,"occurences":1}},"disturbing":{"dreams":{"probability":1,"occurences":1}},"especially":{"with":{"probability":1,"occurences":1}},"household":{"after":{"probability":1,"occurences":1}},"sister,":{"became":{"probability":1,"occurences":1}},"angry":{"at":{"probability":1,"occurences":1}},"formidable":{"intellect":{"probability":1,"occurences":1}},"arguably":{"considered":{"probability":1,"occurences":1}},"sets":{"out":{"probability":1,"occurences":1}},"non-conformism,":{"as":{"probability":1,"occurences":1}},"repression":{"and":{"probability":0.5,"occurences":2},"which,":{"probability":0.5,"occurences":2}},"chapter":{"of":{"probability":1,"occurences":1}},"while":{"under":{"probability":1,"occurences":1}},"entry":{"for":{"probability":1,"occurences":1}},"when":{"he":{"probability":0.5,"occurences":2},"acting":{"probability":0.5,"occurences":2}},"associate":{"his":{"probability":1,"occurences":1}},"discussion":{"of":{"probability":0.25,"occurences":4},"group":{"probability":0.25,"occurences":4},"would":{"probability":0.25,"occurences":4},"group.":{"probability":0.25,"occurences":4}},"1905,":{"Freud":{"probability":1,"occurences":1}},"historian":{"concluded,":{"probability":1,"occurences":1}},"identity":{"were":{"probability":1,"occurences":1}},"worldwide":{"psychoanalytic":{"probability":1,"occurences":1}},"locum":{"in":{"probability":1,"occurences":1}},"sixteen":{"members,":{"probability":1,"occurences":1}},"painting.":{"With":{"probability":1,"occurences":1}},"developed":{"highly":{"probability":0.5,"occurences":2},"as":{"probability":0.5,"occurences":2}},"drawn":{"to":{"probability":1,"occurences":1}},"now":{"known":{"probability":0.5,"occurences":2},"arguing":{"probability":0.5,"occurences":2}},"results":{"of":{"probability":1,"occurences":1}},"With":{"his":{"probability":1,"occurences":1}},"ideas,":{"particularly":{"probability":1,"occurences":1}},"precondition":{"for":{"probability":1,"occurences":1}},"moderating":{"it.":{"probability":1,"occurences":1}},"acting":{"as":{"probability":1,"occurences":1}},"Despite":{"health":{"probability":0.5,"occurences":2},"support":{"probability":0.5,"occurences":2}},"August":{"1898,":{"probability":1,"occurences":1}},"molestation":{"in":{"probability":1,"occurences":1}},"\"an":{"old":{"probability":1,"occurences":1}},"mainstream":{"because":{"probability":1,"occurences":1}},"district":{"of":{"probability":1,"occurences":1}},"build":{"a":{"probability":1,"occurences":1}},"sexuality,":{"describing":{"probability":1,"occurences":1}},"buccal":{"cancer.":{"probability":1,"occurences":1}},"secured":{"only":{"probability":1,"occurences":1}},"Kahane,":{"and":{"probability":0.5,"occurences":2},"who":{"probability":0.5,"occurences":2}},"talking":{"cure\"":{"probability":0.5,"occurences":1},"in":{"probability":0.5,"occurences":1}},"rumours,":{"started":{"probability":1,"occurences":1}},"scenarios":{"still":{"probability":1,"occurences":1}},"surgery":{"proved":{"probability":0.5,"occurences":2},"in":{"probability":0.5,"occurences":2}},"considered":{"to":{"probability":0.5,"occurences":2},"pseudoscientific.":{"probability":0.5,"occurences":2}},"\"neurasthenia\"":{"which":{"probability":1,"occurences":1}},"hysteria.":{"Freud":{"probability":1,"occurences":1}},"with":{"his":{"probability":0.074074074074074,"occurences":54},"Freud":{"probability":0.11111111111111,"occurences":81},"respect":{"probability":0.037037037037037,"occurences":27},"Freud's":{"probability":0.037037037037037,"occurences":27},"this":{"probability":0.037037037037037,"occurences":27},"Freud.":{"probability":0.074074074074074,"occurences":54},"Brill":{"probability":0.037037037037037,"occurences":27},"Carl":{"probability":0.037037037037037,"occurences":27},"Fliess":{"probability":0.074074074074074,"occurences":54},"depth":{"probability":0.037037037037037,"occurences":27},"their":{"probability":0.037037037037037,"occurences":27},"Jean-Martin":{"probability":0.037037037037037,"occurences":27},"patients":{"probability":0.037037037037037,"occurences":27},"prior":{"probability":0.037037037037037,"occurences":27},"the":{"probability":0.11111111111111,"occurences":81},"consequent":{"probability":0.037037037037037,"occurences":27},"Josef":{"probability":0.037037037037037,"occurences":27},"a":{"probability":0.074074074074074,"occurences":54},"establishing":{"probability":0.037037037037037,"occurences":27}},"mobility,":{"as":{"probability":1,"occurences":1}},"mind,":{"his":{"probability":1,"occurences":1}},"explorations":{"of":{"probability":1,"occurences":1}},"or":{"inhibition,":{"probability":0.023809523809524,"occurences":8},"his":{"probability":0.023809523809524,"occurences":8},"to":{"probability":0.023809523809524,"occurences":8},"their":{"probability":0.023809523809524,"occurences":8},"else":{"probability":0.023809523809524,"occurences":8},"masturbation,":{"probability":0.023809523809524,"occurences":8},"docent":{"probability":0.023809523809524,"occurences":8},"evolutionary":{"probability":0.023809523809524,"occurences":8},"the":{"probability":0.11904761904762,"occurences":40},"an":{"probability":0.023809523809524,"occurences":8},"imagined":{"probability":0.023809523809524,"occurences":8},"much":{"probability":0.023809523809524,"occurences":8},"became":{"probability":0.023809523809524,"occurences":8},"Freud's":{"probability":0.095238095238095,"occurences":32},"13":{"probability":0.023809523809524,"occurences":8},"of":{"probability":0.071428571428571,"occurences":24},"ordinarius\"":{"probability":0.023809523809524,"occurences":8},"Students":{"probability":0.023809523809524,"occurences":8},"Lipps":{"probability":0.023809523809524,"occurences":8},"–":{"probability":0.023809523809524,"occurences":8},"her":{"probability":0.023809523809524,"occurences":8},"as":{"probability":0.023809523809524,"occurences":8},"unknown":{"probability":0.023809523809524,"occurences":8},"in":{"probability":0.023809523809524,"occurences":8},"memories":{"probability":0.023809523809524,"occurences":8},"Ferenczi":{"probability":0.023809523809524,"occurences":8},"Freudian":{"probability":0.023809523809524,"occurences":8},"philosophical":{"probability":0.023809523809524,"occurences":8},"extraordinarius\"":{"probability":0.023809523809524,"occurences":8},"Meynert's":{"probability":0.023809523809524,"occurences":8},"a":{"probability":0.047619047619048,"occurences":16},"teaching":{"probability":0.023809523809524,"occurences":8}},"Otto":{"Rank,":{"probability":1,"occurences":1}},"field":{"of":{"probability":1,"occurences":1}},"loyalty":{"to":{"probability":1,"occurences":1}},"by":{"encouraging":{"probability":0.047619047619048,"occurences":21},"birth.":{"probability":0.047619047619048,"occurences":21},"Adler":{"probability":0.047619047619048,"occurences":21},"Freud":{"probability":0.14285714285714,"occurences":63},"Imago,":{"probability":0.047619047619048,"occurences":21},"then":{"probability":0.047619047619048,"occurences":21},"Fechner":{"probability":0.047619047619048,"occurences":21},"habitual":{"probability":0.047619047619048,"occurences":21},"Tatiana":{"probability":0.047619047619048,"occurences":21},"removal":{"probability":0.047619047619048,"occurences":21},"Jung":{"probability":0.047619047619048,"occurences":21},"law\".":{"probability":0.047619047619048,"occurences":21},"Eduard":{"probability":0.047619047619048,"occurences":21},"Rank.":{"probability":0.047619047619048,"occurences":21},"Rank":{"probability":0.047619047619048,"occurences":21},"the":{"probability":0.14285714285714,"occurences":63},"Carl":{"probability":0.047619047619048,"occurences":21}},"university":{"professor.":{"probability":0.33333333333333,"occurences":3},"with":{"probability":0.33333333333333,"occurences":3},"lecturer":{"probability":0.33333333333333,"occurences":3}},"structuring":{"of":{"probability":1,"occurences":1}},"\"talking":{"cure\"":{"probability":1,"occurences":1}},"included":{"the":{"probability":0.5,"occurences":2},"severe":{"probability":0.5,"occurences":2}},"widespread":{"media":{"probability":1,"occurences":1}},"development":{"of":{"probability":1,"occurences":1}},"Everyday":{"Life":{"probability":1,"occurences":1}},"method":{"and":{"probability":1,"occurences":4}},"this":{"stay":{"probability":0.14285714285714,"occurences":7},"respect.":{"probability":0.14285714285714,"occurences":7},"way,":{"probability":0.14285714285714,"occurences":7},"procedure,":{"probability":0.14285714285714,"occurences":7},"account":{"probability":0.14285714285714,"occurences":7},"formative":{"probability":0.14285714285714,"occurences":7},"discussion":{"probability":0.14285714285714,"occurences":7}},"based.":{"Approach":{"probability":0.5,"occurences":2},"An":{"probability":0.5,"occurences":2}},"19":{"Freud's":{"probability":1,"occurences":1}},"underlay":{"symptom":{"probability":1,"occurences":1}},"set":{"up":{"probability":0.5,"occurences":2},"out":{"probability":0.5,"occurences":2}},"was":{"joined":{"probability":0.025,"occurences":40},"launched":{"probability":0.025,"occurences":40},"\"completely":{"probability":0.025,"occurences":40},"using":{"probability":0.025,"occurences":40},"the":{"probability":0.1,"occurences":160},"later":{"probability":0.025,"occurences":40},"published.":{"probability":0.025,"occurences":40},"agreed":{"probability":0.025,"occurences":40},"by":{"probability":0.025,"occurences":40},"invited":{"probability":0.025,"occurences":40},"awarded":{"probability":0.025,"occurences":40},"employed":{"probability":0.025,"occurences":40},"convened":{"probability":0.025,"occurences":40},"seventeen.":{"probability":0.025,"occurences":40},"its":{"probability":0.025,"occurences":40},"followed":{"probability":0.025,"occurences":40},"to":{"probability":0.025,"occurences":40},"developed":{"probability":0.025,"occurences":40},"known":{"probability":0.025,"occurences":40},"important":{"probability":0.025,"occurences":40},"based.":{"probability":0.025,"occurences":40},"conducting":{"probability":0.025,"occurences":40},"always":{"probability":0.025,"occurences":40},"different":{"probability":0.025,"occurences":40},"one":{"probability":0.025,"occurences":40},"reconstituted":{"probability":0.025,"occurences":40},"secured":{"probability":0.025,"occurences":40},"also":{"probability":0.025,"occurences":40},"restored":{"probability":0.025,"occurences":40},"a":{"probability":0.025,"occurences":40},"called":{"probability":0.025,"occurences":40},"published":{"probability":0.025,"occurences":40},"particularly":{"probability":0.025,"occurences":40},"curable":{"probability":0.025,"occurences":40},"elected,":{"probability":0.025,"occurences":40},"reluctant":{"probability":0.025,"occurences":40},"retrospectively":{"probability":0.025,"occurences":40}},"works;":{"he":{"probability":1,"occurences":1}},"Theodor":{"Meynert's":{"probability":0.5,"occurences":2},"Lipps":{"probability":0.5,"occurences":2}},"meet":{"at":{"probability":1,"occurences":1}},"own":{"for":{"probability":0.14285714285714,"occurences":3},"reasons":{"probability":0.14285714285714,"occurences":3},"and":{"probability":0.14285714285714,"occurences":3},"as":{"probability":0.14285714285714,"occurences":3},"to":{"probability":0.14285714285714,"occurences":3},"ideas.":{"probability":0.14285714285714,"occurences":3},"dreams":{"probability":0.14285714285714,"occurences":3}},"reveal,":{"though":{"probability":1,"occurences":1}},"warnings":{"from":{"probability":1,"occurences":1}},"presented":{"as":{"probability":0.5,"occurences":1},"a":{"probability":0.5,"occurences":1}},"(Psychologische":{"Mittwochs-Gesellschaft)":{"probability":1,"occurences":1}},"English":{"throughout":{"probability":0.5,"occurences":2},"translations":{"probability":0.5,"occurences":2}},"some":{"ideas":{"probability":1,"occurences":1}},"enhanced":{"status":{"probability":0.5,"occurences":2},"his":{"probability":0.5,"occurences":2}},"achieved":{"by":{"probability":1,"occurences":1}},"inhibition,":{"about":{"probability":1,"occurences":1}},"movement.":{"Freud":{"probability":1,"occurences":1}},"delicately":{"intimate":{"probability":1,"occurences":1}},"sexual":{"scenarios":{"probability":0.14285714285714,"occurences":7},"periodicity":{"probability":0.14285714285714,"occurences":7},"trauma":{"probability":0.14285714285714,"occurences":7},"abuse,":{"probability":0.14285714285714,"occurences":7},"molestation":{"probability":0.14285714285714,"occurences":7},"identity.":{"probability":0.14285714285714,"occurences":7},"problem":{"probability":0.14285714285714,"occurences":7}},"means":{"of":{"probability":1,"occurences":1}},"childhood":{"friends":{"probability":0.5,"occurences":2},"were":{"probability":0.5,"occurences":2}},"death,":{"Freud":{"probability":1,"occurences":1}},"incidents":{"associated":{"probability":1,"occurences":1}},"been":{"delivering":{"probability":0.125,"occurences":8},"founded":{"probability":0.125,"occurences":8},"suggested":{"probability":0.125,"occurences":8},"presented":{"probability":0.125,"occurences":8},"blocked":{"probability":0.125,"occurences":8},"partially":{"probability":0.125,"occurences":8},"a":{"probability":0.125,"occurences":8},"drawn":{"probability":0.125,"occurences":8}},"memories.":{"This":{"probability":1,"occurences":1}},"psychoneuroses":{"(hysteria":{"probability":1,"occurences":1}},"applied":{"his":{"probability":1,"occurences":1}},"body":{"of":{"probability":1,"occurences":1}},"theory.":{"In":{"probability":1,"occurences":1}},"(hysteria":{"and":{"probability":1,"occurences":1}},"light":{"of":{"probability":1,"occurences":4}},"for":{"his":{"probability":0.047619047619048,"occurences":21},"13":{"probability":0.047619047619048,"occurences":21},"their":{"probability":0.047619047619048,"occurences":21},"unknown":{"probability":0.047619047619048,"occurences":21},"Freud's":{"probability":0.19047619047619,"occurences":84},"the":{"probability":0.23809523809524,"occurences":105},"masturbation,":{"probability":0.047619047619048,"occurences":21},"Freudian":{"probability":0.047619047619048,"occurences":21},"much":{"probability":0.047619047619048,"occurences":21},"an":{"probability":0.047619047619048,"occurences":21},"Students":{"probability":0.047619047619048,"occurences":21},"a":{"probability":0.095238095238095,"occurences":42},"her":{"probability":0.047619047619048,"occurences":21}},"abuse,":{"now":{"probability":1,"occurences":1}},"during":{"a":{"probability":1,"occurences":1}},"function,":{"but":{"probability":1,"occurences":1}},"presupposes":{"an":{"probability":1,"occurences":1}},"only":{"an":{"probability":0.25,"occurences":4},"when":{"probability":0.25,"occurences":4},"with":{"probability":0.25,"occurences":4},"bring":{"probability":0.25,"occurences":4}},"horror":{"–":{"probability":1,"occurences":1}},"place":{"during":{"probability":0.5,"occurences":2},"and":{"probability":0.5,"occurences":2}},"substantial":{"effect":{"probability":0.5,"occurences":2},"body":{"probability":0.5,"occurences":2}},"bleeding,":{"concluded":{"probability":1,"occurences":1}},"19,":{"near":{"probability":1,"occurences":1}},"without":{"blame\",":{"probability":0.5,"occurences":2},"censorship":{"probability":0.5,"occurences":2}},"provided":{"the":{"probability":0.5,"occurences":2},"an":{"probability":0.5,"occurences":2}},"end":{"the":{"probability":0.16666666666667,"occurences":1},"and":{"probability":0.33333333333333,"occurences":2},"Wilhelm":{"probability":0.16666666666667,"occurences":1},"with":{"probability":0.16666666666667,"occurences":1},"Eduard":{"probability":0.16666666666667,"occurences":1}},"self-analysis,":{"Freud":{"probability":1,"occurences":1}},"psychic":{"action":{"probability":1,"occurences":1}},"Unconscious":{"(1869).":{"probability":0.5,"occurences":2},"(1905).":{"probability":0.5,"occurences":2}},"and":{"their":{"probability":0.009009009009009,"occurences":110},"went":{"probability":0.009009009009009,"occurences":110},"in":{"probability":0.027027027027027,"occurences":330},"introspection.":{"probability":0.009009009009009,"occurences":110},"accusing":{"probability":0.009009009009009,"occurences":110},"these":{"probability":0.009009009009009,"occurences":110},"entered":{"probability":0.009009009009009,"occurences":110},"conscious)":{"probability":0.009009009009009,"occurences":110},"throat":{"probability":0.009009009009009,"occurences":110},"all":{"probability":0.009009009009009,"occurences":110},"Ferenczi,":{"probability":0.009009009009009,"occurences":110},"Jones,":{"probability":0.009009009009009,"occurences":110},"obsessional":{"probability":0.009009009009009,"occurences":110},"Herbart":{"probability":0.009009009009009,"occurences":110},"attend":{"probability":0.009009009009009,"occurences":110},"that":{"probability":0.027027027027027,"occurences":330},"to":{"probability":0.009009009009009,"occurences":110},"periods":{"probability":0.009009009009009,"occurences":110},"Rudolf":{"probability":0.009009009009009,"occurences":110},"personal":{"probability":0.009009009009009,"occurences":110},"notable":{"probability":0.009009009009009,"occurences":110},"Jung,":{"probability":0.009009009009009,"occurences":110},"they":{"probability":0.009009009009009,"occurences":110},"certain":{"probability":0.009009009009009,"occurences":110},"Practicing":{"probability":0.009009009009009,"occurences":110},"empathy.":{"probability":0.009009009009009,"occurences":110},"Sabina":{"probability":0.009009009009009,"occurences":110},"father":{"probability":0.009009009009009,"occurences":110},"memories":{"probability":0.009009009009009,"occurences":110},"psychology":{"probability":0.009009009009009,"occurences":110},"a":{"probability":0.027027027027027,"occurences":330},"literary":{"probability":0.009009009009009,"occurences":110},"joined":{"probability":0.009009009009009,"occurences":110},"neuropathology.":{"probability":0.009009009009009,"occurences":110},"came":{"probability":0.009009009009009,"occurences":110},"both":{"probability":0.009009009009009,"occurences":110},"genitalia":{"probability":0.009009009009009,"occurences":110},"emotional":{"probability":0.009009009009009,"occurences":110},"the":{"probability":0.09009009009009,"occurences":1100},"left":{"probability":0.009009009009009,"occurences":110},"moral":{"probability":0.009009009009009,"occurences":110},"Nietzsche,":{"probability":0.009009009009009,"occurences":110},"were":{"probability":0.009009009009009,"occurences":110},"set":{"probability":0.009009009009009,"occurences":110},"was":{"probability":0.009009009009009,"occurences":110},"Jung.":{"probability":0.009009009009009,"occurences":110},"atmosphere":{"probability":0.009009009009009,"occurences":110},"effective":{"probability":0.009009009009009,"occurences":110},"sinuses":{"probability":0.009009009009009,"occurences":110},"clinical":{"probability":0.009009009009009,"occurences":110},"Anna":{"probability":0.009009009009009,"occurences":110},"secretary":{"probability":0.009009009009009,"occurences":110},"controversial":{"probability":0.009009009009009,"occurences":110},"graduates":{"probability":0.009009009009009,"occurences":110},"as":{"probability":0.009009009009009,"occurences":110},"theoretical":{"probability":0.009009009009009,"occurences":110},"subsequently":{"probability":0.009009009009009,"occurences":110},"his":{"probability":0.045045045045045,"occurences":550},"cigarettes":{"probability":0.009009009009009,"occurences":110},"menstrual)":{"probability":0.009009009009009,"occurences":110},"Jones":{"probability":0.018018018018018,"occurences":220},"censorship":{"probability":0.009009009009009,"occurences":110},"rivalrous":{"probability":0.009009009009009,"occurences":110},"Ludwig":{"probability":0.009009009009009,"occurences":110},"irregular":{"probability":0.009009009009009,"occurences":110},"collaborator,":{"probability":0.009009009009009,"occurences":110},"revise":{"probability":0.009009009009009,"occurences":110},"decisive":{"probability":0.009009009009009,"occurences":110},"Stekel,":{"probability":0.009009009009009,"occurences":110},"susceptibility":{"probability":0.009009009009009,"occurences":110},"it":{"probability":0.027027027027027,"occurences":330},"made":{"probability":0.009009009009009,"occurences":110},"menstrual":{"probability":0.009009009009009,"occurences":110},"Reitler":{"probability":0.018018018018018,"occurences":220},"Zürich":{"probability":0.009009009009009,"occurences":110},"bisexuality":{"probability":0.009009009009009,"occurences":110},"prestige":{"probability":0.009009009009009,"occurences":110},"\"the":{"probability":0.009009009009009,"occurences":110},"psychiatrist":{"probability":0.009009009009009,"occurences":110},"medical":{"probability":0.009009009009009,"occurences":110},"away":{"probability":0.009009009009009,"occurences":110},"attracted":{"probability":0.009009009009009,"occurences":110},"Max":{"probability":0.009009009009009,"occurences":110},"Jokes":{"probability":0.009009009009009,"occurences":110},"which":{"probability":0.009009009009009,"occurences":110},"triggered":{"probability":0.009009009009009,"occurences":110},"cakes":{"probability":0.009009009009009,"occurences":110},"begun":{"probability":0.009009009009009,"occurences":110}},"Studies":{"on":{"probability":1,"occurences":1}},"Hysteria":{"published":{"probability":1,"occurences":1}},"1895":{"(co-authored":{"probability":1,"occurences":1}},"Galvanic":{"Skin":{"probability":1,"occurences":1}},"(she":{"would":{"probability":1,"occurences":1}},"endorse":{"his":{"probability":1,"occurences":1}},"Baroness":{"Marie":{"probability":1,"occurences":1}},"Dreams":{"in":{"probability":1,"occurences":1}},"Minna":{"Bernays,":{"probability":1,"occurences":1}},"onset.":{"The":{"probability":1,"occurences":1}},"paper.":{"Then,":{"probability":1,"occurences":1}},"und":{"psychopathologishe":{"probability":0.5,"occurences":1},"that":{"probability":0.5,"occurences":1}},"theories,":{"attention":{"probability":0.5,"occurences":2},"caused":{"probability":0.5,"occurences":2}},"saw":{"themselves":{"probability":1,"occurences":1}},"shared":{"Freud's":{"probability":1,"occurences":1}},"subsequent":{"removal":{"probability":0.2,"occurences":5},"formulation":{"probability":0.2,"occurences":5},"letters":{"probability":0.2,"occurences":5},"importance":{"probability":0.2,"occurences":5},"public":{"probability":0.2,"occurences":5}},"go":{"on":{"probability":1,"occurences":1}},"pre-conscious":{"and":{"probability":1,"occurences":1}},"Dreams,":{"was":{"probability":0.5,"occurences":2},"to":{"probability":0.5,"occurences":2}},"audience":{"included":{"probability":1,"occurences":1}},"had":{"written":{"probability":0.03448275862069,"occurences":29},"to":{"probability":0.03448275862069,"occurences":29},"kept":{"probability":0.03448275862069,"occurences":29},"postulated":{"probability":0.03448275862069,"occurences":29},"six":{"probability":0.03448275862069,"occurences":29},"been":{"probability":0.13793103448276,"occurences":116},"concluded,":{"probability":0.03448275862069,"occurences":29},"attended":{"probability":0.03448275862069,"occurences":29},"left":{"probability":0.03448275862069,"occurences":29},"opened":{"probability":0.03448275862069,"occurences":29},"decided":{"probability":0.03448275862069,"occurences":29},"reached":{"probability":0.03448275862069,"occurences":29},"set":{"probability":0.03448275862069,"occurences":29},"met":{"probability":0.03448275862069,"occurences":29},"first":{"probability":0.068965517241379,"occurences":58},"Fliess":{"probability":0.03448275862069,"occurences":29},"grown":{"probability":0.03448275862069,"occurences":29},"not":{"probability":0.03448275862069,"occurences":29},"called":{"probability":0.03448275862069,"occurences":29},"expressed":{"probability":0.03448275862069,"occurences":29},"greatly":{"probability":0.03448275862069,"occurences":29},"discovered":{"probability":0.03448275862069,"occurences":29},"studied,":{"probability":0.03448275862069,"occurences":29},"a":{"probability":0.03448275862069,"occurences":29},"studied":{"probability":0.03448275862069,"occurences":29}},"respectively.":{"Brill":{"probability":1,"occurences":1}},"\"drives\",":{"to":{"probability":1,"occurences":1}},"Hysteria,":{"which":{"probability":1,"occurences":1}},"impact":{"of":{"probability":1,"occurences":4}},"physician":{"Wilhelm":{"probability":1,"occurences":1}},"studied,":{"in":{"probability":1,"occurences":1}},"dreams":{"in":{"probability":0.25,"occurences":4},"could":{"probability":0.25,"occurences":4},"and":{"probability":0.5,"occurences":8}},"group.":{"Thereafter,":{"probability":1,"occurences":1}},"throat":{"specialist":{"probability":1,"occurences":1}},"met":{"Jung":{"probability":0.25,"occurences":4},"together":{"probability":0.25,"occurences":4},"up":{"probability":0.25,"occurences":4},"1887.":{"probability":0.25,"occurences":4}},"isolated":{"from":{"probability":1,"occurences":1}},"ambitions":{"to":{"probability":1,"occurences":1}},"Rudolf":{"Reitler,":{"probability":1,"occurences":1}},"censorship":{"of":{"probability":0.5,"occurences":2},"or":{"probability":0.5,"occurences":2}},"nasogenital":{"connection":{"probability":1,"occurences":1}},"research.":{"Charcot":{"probability":1,"occurences":1}},"period,":{"Freud":{"probability":1,"occurences":1}},"expressed":{"interest":{"probability":1,"occurences":1}},"friend":{"Eduard":{"probability":0.25,"occurences":4},"and":{"probability":0.5,"occurences":8},"Wilhelm":{"probability":0.25,"occurences":4}},"leg":{"pains":{"probability":1,"occurences":1}},"aspects":{"of":{"probability":1,"occurences":1}},"ritual":{"and":{"probability":1,"occurences":1}},"interruptus,":{"and":{"probability":1,"occurences":1}},"Study,":{"published":{"probability":1,"occurences":1}},"what":{"were":{"probability":1,"occurences":1}},"symptoms.":{"They":{"probability":1,"occurences":1}},"denied":{"its":{"probability":1,"occurences":1}},"elaborate":{"and":{"probability":1,"occurences":1}},"\"free":{"association\",":{"probability":1,"occurences":1}},"exercise":{"self-control":{"probability":1,"occurences":1}},"relationship":{"came":{"probability":0.33333333333333,"occurences":3},"became":{"probability":0.33333333333333,"occurences":3},"she":{"probability":0.33333333333333,"occurences":3}},"unwillingness":{"to":{"probability":1,"occurences":1}},"hypnosis,":{"which":{"probability":0.5,"occurences":2},"having":{"probability":0.5,"occurences":2}},"letters":{"to":{"probability":0.5,"occurences":2},"maintained":{"probability":0.5,"occurences":2}},"cure\"":{"for":{"probability":1,"occurences":1}},"death":{"of":{"probability":1,"occurences":4}},"lectures.":{"In":{"probability":1,"occurences":1}},"reached":{"an":{"probability":1,"occurences":1}},"outside":{"the":{"probability":1,"occurences":1}},"subsequently":{"referred":{"probability":0.5,"occurences":2},"gave":{"probability":0.5,"occurences":2}},"well":{"as":{"probability":1,"occurences":1}},"him.":{"According":{"probability":1,"occurences":1}},"worked":{"in":{"probability":1,"occurences":1}},"women":{"would":{"probability":1,"occurences":1}},"political":{"authorities":{"probability":1,"occurences":1}},"coitus":{"interruptus,":{"probability":1,"occurences":1}},"according":{"to":{"probability":1,"occurences":1}},"local":{"asylum":{"probability":1,"occurences":1}},"effective":{"symptom":{"probability":1,"occurences":1}},"masturbation":{"which,":{"probability":1,"occurences":1}},"relating":{"to":{"probability":1,"occurences":1}},"attachment":{"and":{"probability":1,"occurences":1}},"told":{"his":{"probability":1,"occurences":1}},"palliative":{"effects":{"probability":1,"occurences":1}},"material":{"and":{"probability":1,"occurences":1}},"various":{"departments":{"probability":1,"occurences":1}},"autonomous":{"infantile":{"probability":1,"occurences":1}},"first,":{"though":{"probability":1,"occurences":1}},"position":{"he":{"probability":1,"occurences":1}},"bring":{"himself":{"probability":1,"occurences":1}},"symptom":{"relief":{"probability":0.5,"occurences":2},"formation.":{"probability":0.5,"occurences":2}},"days.":{"Putnam's":{"probability":1,"occurences":1}},"version,":{"On":{"probability":1,"occurences":1}},"ultimately,":{"in":{"probability":1,"occurences":1}},"away":{"from":{"probability":1,"occurences":1}},"patient":{"of":{"probability":0.5,"occurences":6},"psychotherapy":{"probability":0.25,"occurences":3},"Emma":{"probability":0.25,"occurences":3}},"meetings":{"of":{"probability":1,"occurences":1}},"meeting,":{"which":{"probability":1,"occurences":1}},"intellectual":{"and":{"probability":0.66666666666667,"occurences":6},"non-conformism,":{"probability":0.33333333333333,"occurences":3}},"taken":{"up":{"probability":0.5,"occurences":2},"at":{"probability":0.5,"occurences":2}},"affection\".":{"Eckstein":{"probability":1,"occurences":1}},"analysis":{"is":{"probability":0.25,"occurences":1},"herself.":{"probability":0.25,"occurences":1},"to":{"probability":0.25,"occurences":1},"with":{"probability":0.25,"occurences":1}},"First":{"one":{"probability":1,"occurences":1}},"docent":{"in":{"probability":0.5,"occurences":2},"of":{"probability":0.5,"occurences":2}},"Zurich":{"University,":{"probability":1,"occurences":1}},"1895).":{"From":{"probability":1,"occurences":1}},"resisted":{"far":{"probability":1,"occurences":1}},"general":{"readership,":{"probability":0.33333333333333,"occurences":3},"explanation":{"probability":0.33333333333333,"occurences":3},"theory":{"probability":0.33333333333333,"occurences":3}},"present,":{"half":{"probability":1,"occurences":1}},"plagiarism":{"of":{"probability":1,"occurences":1}},"of":{"their":{"probability":0.017142857142857,"occurences":525},"Nietzsche's":{"probability":0.0057142857142857,"occurences":175},"Adler":{"probability":0.0057142857142857,"occurences":175},"underrated":{"probability":0.0057142857142857,"occurences":175},"Isaac":{"probability":0.0057142857142857,"occurences":175},"these":{"probability":0.0057142857142857,"occurences":175},"adolescent":{"probability":0.0057142857142857,"occurences":175},"an":{"probability":0.04,"occurences":1225},"Breuer's":{"probability":0.0057142857142857,"occurences":175},"hysteria":{"probability":0.0057142857142857,"occurences":175},"perception":{"probability":0.0057142857142857,"occurences":175},"Sexuality,":{"probability":0.0057142857142857,"occurences":175},"repression":{"probability":0.0057142857142857,"occurences":175},"Viennese":{"probability":0.0057142857142857,"occurences":175},"Stanley":{"probability":0.0057142857142857,"occurences":175},"how":{"probability":0.0057142857142857,"occurences":175},"talking":{"probability":0.0057142857142857,"occurences":175},"Dreams.":{"probability":0.0057142857142857,"occurences":175},"unconscious":{"probability":0.0057142857142857,"occurences":175},"desire":{"probability":0.0057142857142857,"occurences":175},"importance":{"probability":0.0057142857142857,"occurences":175},"childhood.":{"probability":0.0057142857142857,"occurences":175},"\"rearousing":{"probability":0.0057142857142857,"occurences":175},"her":{"probability":0.011428571428571,"occurences":350},"cultural":{"probability":0.0057142857142857,"occurences":175},"what":{"probability":0.0057142857142857,"occurences":175},"case":{"probability":0.0057142857142857,"occurences":175},"certain":{"probability":0.0057142857142857,"occurences":175},"hostility":{"probability":0.0057142857142857,"occurences":175},"hysteria,":{"probability":0.0057142857142857,"occurences":175},"published":{"probability":0.0057142857142857,"occurences":175},"1902,":{"probability":0.0057142857142857,"occurences":175},"Vienna.":{"probability":0.011428571428571,"occurences":350},"Vienna":{"probability":0.011428571428571,"occurences":350},"psychoanalysts":{"probability":0.0057142857142857,"occurences":175},"a":{"probability":0.022857142857143,"occurences":700},"biology\",":{"probability":0.0057142857142857,"occurences":175},"education":{"probability":0.0057142857142857,"occurences":175},"labour":{"probability":0.0057142857142857,"occurences":175},"hypnosis":{"probability":0.0057142857142857,"occurences":175},"both":{"probability":0.011428571428571,"occurences":350},"psychoanalysis":{"probability":0.0057142857142857,"occurences":175},"gauze":{"probability":0.0057142857142857,"occurences":175},"psychoanalytic":{"probability":0.0057142857142857,"occurences":175},"whom":{"probability":0.011428571428571,"occurences":350},"Jung":{"probability":0.0057142857142857,"occurences":175},"Everyday":{"probability":0.0057142857142857,"occurences":175},"this":{"probability":0.0057142857142857,"occurences":175},"Jung.":{"probability":0.0057142857142857,"occurences":175},"Hysteria,":{"probability":0.0057142857142857,"occurences":175},"four":{"probability":0.0057142857142857,"occurences":175},"part":{"probability":0.0057142857142857,"occurences":175},"psychiatry.":{"probability":0.0057142857142857,"occurences":175},"depression,":{"probability":0.0057142857142857,"occurences":175},"Sexuality":{"probability":0.0057142857142857,"occurences":175},"Eckstein's":{"probability":0.011428571428571,"occurences":350},"Toronto":{"probability":0.0057142857142857,"occurences":175},"sexual":{"probability":0.017142857142857,"occurences":525},"mental":{"probability":0.0057142857142857,"occurences":175},"Diseases":{"probability":0.0057142857142857,"occurences":175},"his":{"probability":0.10857142857143,"occurences":3325},"\"Little":{"probability":0.0057142857142857,"occurences":175},"Fliess's":{"probability":0.0057142857142857,"occurences":175},"cocaine":{"probability":0.0057142857142857,"occurences":175},"infantile":{"probability":0.017142857142857,"occurences":525},"human":{"probability":0.011428571428571,"occurences":350},"sexuality":{"probability":0.0057142857142857,"occurences":175},"collusion":{"probability":0.0057142857142857,"occurences":175},"medical":{"probability":0.0057142857142857,"occurences":175},"wish-fulfillments":{"probability":0.0057142857142857,"occurences":175},"lectures":{"probability":0.0057142857142857,"occurences":175},"sexuality.":{"probability":0.0057142857142857,"occurences":175},"The":{"probability":0.0057142857142857,"occurences":175},"\"professor":{"probability":0.0057142857142857,"occurences":175},"Tragedy":{"probability":0.0057142857142857,"occurences":175},"significant":{"probability":0.0057142857142857,"occurences":175},"Freud's":{"probability":0.04,"occurences":1225},"psychological":{"probability":0.0057142857142857,"occurences":175},"symptoms":{"probability":0.0057142857142857,"occurences":175},"Charles":{"probability":0.0057142857142857,"occurences":175},"existing":{"probability":0.0057142857142857,"occurences":175},"the":{"probability":0.18285714285714,"occurences":5600},"one":{"probability":0.011428571428571,"occurences":350},"condoms":{"probability":0.0057142857142857,"occurences":175},"1910":{"probability":0.0057142857142857,"occurences":175},"Dreams,":{"probability":0.0057142857142857,"occurences":175},"Internal":{"probability":0.0057142857142857,"occurences":175},"Freud.":{"probability":0.0057142857142857,"occurences":175},"traumatic":{"probability":0.0057142857142857,"occurences":175},"Theodor":{"probability":0.0057142857142857,"occurences":175},"collaboration":{"probability":0.0057142857142857,"occurences":175},"Clark":{"probability":0.0057142857142857,"occurences":175},"which":{"probability":0.011428571428571,"occurences":350},"Ernest":{"probability":0.0057142857142857,"occurences":175},"neutralizing":{"probability":0.0057142857142857,"occurences":175},"Dreams":{"probability":0.0057142857142857,"occurences":175}},"title":{"\"professor":{"probability":1,"occurences":1}},"may":{"have":{"probability":1,"occurences":1}},"heart":{"irregularities,":{"probability":1,"occurences":1}},"respect":{"to":{"probability":0.5,"occurences":2},"of":{"probability":0.5,"occurences":2}},"Bernays,":{"the":{"probability":0.33333333333333,"occurences":3},"a":{"probability":0.33333333333333,"occurences":3},"Martha":{"probability":0.33333333333333,"occurences":3}},"1920).":{"Despite":{"probability":1,"occurences":1}},"consistent":{"and":{"probability":0.5,"occurences":1},"results":{"probability":0.5,"occurences":1}},"give":{"lectures":{"probability":1,"occurences":1}},"organised":{"the":{"probability":1,"occurences":1}},"granted":{"the":{"probability":1,"occurences":1}},"salary":{"or":{"probability":1,"occurences":1}},"treat":{"\"nasal":{"probability":0.33333333333333,"occurences":2},"where":{"probability":0.33333333333333,"occurences":2},"Nietzsche's":{"probability":0.33333333333333,"occurences":2}},"susceptibility":{"to":{"probability":1,"occurences":1}},"prevailing":{"methods":{"probability":0.5,"occurences":2},"clinical":{"probability":0.5,"occurences":2}},"made":{"the":{"probability":0.25,"occurences":4},"subject":{"probability":0.25,"occurences":4},"a":{"probability":0.25,"occurences":4},"use":{"probability":0.25,"occurences":4}},"bribe":{"the":{"probability":1,"occurences":1}},"patients":{"on":{"probability":0.5,"occurences":2},"to":{"probability":0.5,"occurences":2}},"mid-1880s":{"as":{"probability":1,"occurences":1}},"ambition":{"to":{"probability":1,"occurences":4}},"which":{"would":{"probability":0.05,"occurences":20},"was":{"probability":0.1,"occurences":40},"are":{"probability":0.05,"occurences":20},"it":{"probability":0.1,"occurences":40},"Freud":{"probability":0.1,"occurences":40},"left":{"probability":0.05,"occurences":20},"he":{"probability":0.3,"occurences":120},"had":{"probability":0.05,"occurences":20},"became":{"probability":0.05,"occurences":20},"entitled":{"probability":0.05,"occurences":20},"prompted":{"probability":0.05,"occurences":20},"this":{"probability":0.05,"occurences":20}},"particularly":{"in":{"probability":0.5,"occurences":2},"interested":{"probability":0.5,"occurences":2}},"led":{"the":{"probability":0.13333333333333,"occurences":12},"interpretations":{"probability":0.066666666666667,"occurences":6},"to":{"probability":0.4,"occurences":36},"\"free":{"probability":0.066666666666667,"occurences":6},"him":{"probability":0.2,"occurences":18},"from":{"probability":0.066666666666667,"occurences":6},"Fliess":{"probability":0.066666666666667,"occurences":6}},"Marie":{"Ferstel,":{"probability":1,"occurences":1}}} \ No newline at end of file diff --git a/resources/markov_preset_generator/markov.lua b/resources/markov_preset_generator/markov.lua new file mode 100644 index 0000000..debe1fe --- /dev/null +++ b/resources/markov_preset_generator/markov.lua @@ -0,0 +1,122 @@ +local markov = {} + +local function node(relations) + local node = {} + local total = 0 + for k,v in pairs(relations) do + total = total + v.occurences + end + for k,v in pairs(relations) do + node[k] = {probability = v.occurences/total,occurences = v.occurences} + end + return node +end + +local function escape(str) + return str:gsub("([%%%*%(%)%^%.%[%]%+%-%$%?])","%%%1") +end + +local function register_words(str,word_list) + local word_list = word_list or {} + str:gsub("%S+",function(word) + if not word_list[word] then + word_list[word] = {} + end + local current_word = word_list[word] + str:gsub(escape(word) .. "%s+(%S+)",function(word2) + if not current_word[word2] then + current_word[word2] = {} + end + if not current_word[word2].occurences then + current_word[word2].occurences = 1 + else + current_word[word2].occurences = current_word[word2].occurences + 1 + end + end) + end) + for k,v in pairs(word_list) do + word_list[k] = node(v) + end + return word_list +end + +local table_length = function(tab) + local len = 0 + for k,v in pairs(tab) do + len = len + 1 + end + return len +end + +function markov.walk(self,start) + if not self.init then + error("Attempted to use an instance method on an uninitialized instance") + end + local random = math.random(0,1e7)/1e7 + local words = {} + words.count = 0 + local word = nil + if self.net[start] then + while (words.count < 1) and (table_length(self.net[start]) > 0) do + for k,v in pairs(self.net[start]) do + if (random <= v.probability) then + words.count = words.count + 1 + table.insert(words,k) + end + end + random = math.random(0,1e7)/1e7 + end + end + if words.count > 0 then + word = words[math.random(1,#words)] + end + return word +end + +function markov.expand_vocabulary(self,source) + if not self.init then + error("Attempted to use an instance method on an uninitialized instance") + end + self.net = register_words(source,self.net) +end + +function markov.save_state(self) + return self.net +end + +function markov.load_state(self,new_state) + self.net = new_state +end + +function markov.run(self,start,count) + if not self.init then + error("Attempted to use an instance method on an uninitialized instance") + end + if not start then + for k,v in pairs(self.net) do + start = k + break + end + end + local sequence = "" + local current_word = start + while current_word do + sequence = sequence..current_word.." " + local _,counter = sequence:gsub("(%S+)","%1") + current_word = self:walk(current_word) + if counter > (count or 200) then + sequence = sequence:sub(1,-2).."..." + break + end + end + return sequence +end + +function markov.new(str) + local self = setmetatable({},{__index = markov}) + self.net = register_words(str or "") + self.init = true + return self +end + +return markov diff --git a/resources/markov_preset_generator/preset_generator.lua b/resources/markov_preset_generator/preset_generator.lua new file mode 100644 index 0000000..1ce598c --- /dev/null +++ b/resources/markov_preset_generator/preset_generator.lua @@ -0,0 +1,11 @@ +markov = require("markov") +json = require("json") +text = [[ + +]] +markov_instance = markov.new(text) +preset = io.open("new_preset.json","w") +a = markov_instance:save_state() +print(a) +preset:write(json.encode(a)) +preset:close() diff --git a/resources/reddit.json b/resources/reddit.json new file mode 100644 index 0000000..b367574 --- /dev/null +++ b/resources/reddit.json @@ -0,0 +1 @@ +{"loser.":{"You":{"probability":1,"occurences":1}},"weight":{"and":{"probability":1,"occurences":1}},"where":{"I":{"probability":0.125,"occurences":8},"to":{"probability":0.375,"occurences":24},"I'm":{"probability":0.25,"occurences":16},"everything's":{"probability":0.125,"occurences":8},"it":{"probability":0.125,"occurences":8}},"well,":{"but":{"probability":1,"occurences":4}},"re-file":{"the":{"probability":1,"occurences":1}},"believed":{"that":{"probability":1,"occurences":1}},"kitten.":{"The":{"probability":1,"occurences":1}},"cloak":{"or":{"probability":1,"occurences":1}},"What":{"I":{"probability":0.2,"occurences":4},"are":{"probability":0.2,"occurences":4},"if":{"probability":0.2,"occurences":4},"the":{"probability":0.4,"occurences":8}},"them":{"if":{"probability":0.125,"occurences":8},"or":{"probability":0.125,"occurences":8},"learn.":{"probability":0.125,"occurences":8},"to":{"probability":0.375,"occurences":24},"as":{"probability":0.125,"occurences":8},"out,":{"probability":0.125,"occurences":8}},"divorce":{"on":{"probability":1,"occurences":1}},"doxing":{"the":{"probability":1,"occurences":1}},"worth.":{"You":{"probability":1,"occurences":4}},"keep":{"the":{"probability":0.5,"occurences":8},"a":{"probability":0.25,"occurences":4},"up.":{"probability":0.25,"occurences":4}},"through":{"so":{"probability":0.5,"occurences":2},"this.":{"probability":0.5,"occurences":2}},"anybody.":{"There":{"probability":1,"occurences":1}},"wouldn't":{"want":{"probability":1,"occurences":1}},"myself.":{"It's":{"probability":1,"occurences":1}},"though":{"he":{"probability":1,"occurences":1}},"talking":{"together.":{"probability":0.33333333333333,"occurences":3},"about":{"probability":0.33333333333333,"occurences":3},"about.":{"probability":0.33333333333333,"occurences":3}},"used":{"about":{"probability":0.5,"occurences":1},"by":{"probability":0.5,"occurences":1}},"much":{"more":{"probability":0.25,"occurences":4},"that":{"probability":0.25,"occurences":4},"and":{"probability":0.25,"occurences":4},"\"change\"":{"probability":0.25,"occurences":4}},"Muslim.":{"I":{"probability":1,"occurences":1}},"work":{"with.":{"probability":0.14285714285714,"occurences":7},"there.":{"probability":0.14285714285714,"occurences":7},"less.":{"probability":0.14285714285714,"occurences":7},"in":{"probability":0.14285714285714,"occurences":7},"harder.":{"probability":0.14285714285714,"occurences":7},"is":{"probability":0.14285714285714,"occurences":7},"for":{"probability":0.14285714285714,"occurences":7}},"help":{"but":{"probability":0.33333333333333,"occurences":3},"her":{"probability":0.33333333333333,"occurences":3},"them":{"probability":0.33333333333333,"occurences":3}},"are":{"automatically":{"probability":0.11627906976744,"occurences":200},"talented.":{"probability":0.023255813953488,"occurences":40},"able":{"probability":0.023255813953488,"occurences":40},"doing":{"probability":0.046511627906977,"occurences":80},"harassing":{"probability":0.023255813953488,"occurences":40},"of":{"probability":0.046511627906977,"occurences":80},"they":{"probability":0.023255813953488,"occurences":40},"married":{"probability":0.023255813953488,"occurences":40},"strong.":{"probability":0.023255813953488,"occurences":40},"a":{"probability":0.13953488372093,"occurences":240},"people":{"probability":0.023255813953488,"occurences":40},"harassing.":{"probability":0.023255813953488,"occurences":40},"an":{"probability":0.023255813953488,"occurences":40},"doing.":{"probability":0.023255813953488,"occurences":40},"tired":{"probability":0.023255813953488,"occurences":40},"going":{"probability":0.093023255813953,"occurences":160},"what":{"probability":0.023255813953488,"occurences":40},"beautiful.":{"probability":0.023255813953488,"occurences":40},"smart.":{"probability":0.023255813953488,"occurences":40},"the":{"probability":0.093023255813953,"occurences":160},"saying":{"probability":0.023255813953488,"occurences":40},"worth.":{"probability":0.023255813953488,"occurences":40},"some":{"probability":0.046511627906977,"occurences":80},"still":{"probability":0.046511627906977,"occurences":80}},"costs":{"of":{"probability":1,"occurences":1}},"even":{"pace":{"probability":0.125,"occurences":8},"keep":{"probability":0.125,"occurences":8},"then":{"probability":0.125,"occurences":8},"though":{"probability":0.125,"occurences":8},"play":{"probability":0.125,"occurences":8},"know":{"probability":0.375,"occurences":24}},"\"What":{"the":{"probability":1,"occurences":1}},"bring":{"you":{"probability":1,"occurences":1}},"what":{"you":{"probability":0.2962962962963,"occurences":216},"to":{"probability":0.33333333333333,"occurences":243},"guys":{"probability":0.037037037037037,"occurences":27},"sucks.":{"probability":0.037037037037037,"occurences":27},"I'm":{"probability":0.11111111111111,"occurences":81},"I":{"probability":0.037037037037037,"occurences":27},"he":{"probability":0.037037037037037,"occurences":27},"the":{"probability":0.037037037037037,"occurences":27},"a":{"probability":0.037037037037037,"occurences":27},"this":{"probability":0.037037037037037,"occurences":27}},"they":{"have":{"probability":0.08,"occurences":50},"are":{"probability":0.04,"occurences":25},"need":{"probability":0.08,"occurences":50},"truly":{"probability":0.04,"occurences":25},"say":{"probability":0.04,"occurences":25},"were":{"probability":0.04,"occurences":25},"don't":{"probability":0.2,"occurences":125},"love.":{"probability":0.04,"occurences":25},"should":{"probability":0.12,"occurences":75},"got":{"probability":0.04,"occurences":25},"still":{"probability":0.04,"occurences":25},"so":{"probability":0.04,"occurences":25},"ask":{"probability":0.04,"occurences":25},"date":{"probability":0.04,"occurences":25},"turn":{"probability":0.04,"occurences":25},"think":{"probability":0.04,"occurences":25},"get":{"probability":0.04,"occurences":25}},"at":{"someone":{"probability":0.0068027210884354,"occurences":4},"their":{"probability":0.0068027210884354,"occurences":4},"around":{"probability":0.0068027210884354,"occurences":4},"everyone":{"probability":0.020408163265306,"occurences":12},"first":{"probability":0.0068027210884354,"occurences":4},"the":{"probability":0.074829931972789,"occurences":44},"job,":{"probability":0.0068027210884354,"occurences":4},"degree,":{"probability":0.0068027210884354,"occurences":4},"this":{"probability":0.0068027210884354,"occurences":4},"you":{"probability":0.22448979591837,"occurences":132},"kid.":{"probability":0.0068027210884354,"occurences":4},"guys":{"probability":0.0068027210884354,"occurences":4},"was":{"probability":0.0068027210884354,"occurences":4},"your":{"probability":0.0068027210884354,"occurences":4},"I'll":{"probability":0.013605442176871,"occurences":8},"I":{"probability":0.068027210884354,"occurences":40},"our":{"probability":0.013605442176871,"occurences":8},"that":{"probability":0.0068027210884354,"occurences":4},"to":{"probability":0.061224489795918,"occurences":36},"are":{"probability":0.0068027210884354,"occurences":4},"if":{"probability":0.074829931972789,"occurences":44},"bring":{"probability":0.0068027210884354,"occurences":4},"he":{"probability":0.013605442176871,"occurences":8},"they":{"probability":0.020408163265306,"occurences":12},"doesn't":{"probability":0.013605442176871,"occurences":8},"chance":{"probability":0.0068027210884354,"occurences":4},"pays":{"probability":0.0068027210884354,"occurences":4},"people":{"probability":0.12244897959184,"occurences":72},"matters.":{"probability":0.0068027210884354,"occurences":4},"sucks.":{"probability":0.0068027210884354,"occurences":4},"I'm":{"probability":0.040816326530612,"occurences":24},"is":{"probability":0.0068027210884354,"occurences":4},"it":{"probability":0.013605442176871,"occurences":8},"place":{"probability":0.0068027210884354,"occurences":4},"husband":{"probability":0.0068027210884354,"occurences":4},"it's":{"probability":0.0068027210884354,"occurences":4},"father":{"probability":0.0068027210884354,"occurences":4},"his":{"probability":0.020408163265306,"occurences":12},"there's":{"probability":0.0068027210884354,"occurences":4},"a":{"probability":0.027210884353741,"occurences":16},"she's":{"probability":0.013605442176871,"occurences":8}},"tomorrow":{"and":{"probability":0.5,"occurences":2},"when":{"probability":0.5,"occurences":2}},"them.":{"The":{"probability":0.25,"occurences":4},"So":{"probability":0.25,"occurences":4},"This":{"probability":0.25,"occurences":4},"I'm":{"probability":0.25,"occurences":4}},"mail":{"shines":{"probability":0.5,"occurences":2},"then":{"probability":0.5,"occurences":2}},"met":{"this":{"probability":1,"occurences":1}},"love":{"each":{"probability":0.1,"occurences":10},"something":{"probability":0.1,"occurences":10},"conspiracy":{"probability":0.1,"occurences":10},"building":{"probability":0.1,"occurences":10},"learning,":{"probability":0.1,"occurences":10},"helping":{"probability":0.1,"occurences":10},"it.":{"probability":0.1,"occurences":10},"making":{"probability":0.1,"occurences":10},"and":{"probability":0.2,"occurences":20}},"it":{"seems":{"probability":0.0625,"occurences":46},"alleges":{"probability":0.03125,"occurences":23},"and":{"probability":0.09375,"occurences":69},"would":{"probability":0.09375,"occurences":69},"in":{"probability":0.03125,"occurences":23},"is":{"probability":0.3125,"occurences":230},"the":{"probability":0.03125,"occurences":23},"was":{"probability":0.15625,"occurences":115},"gets":{"probability":0.03125,"occurences":23},"on":{"probability":0.03125,"occurences":23},"of":{"probability":0.03125,"occurences":23},"skeptical":{"probability":0.03125,"occurences":23},"as":{"probability":0.03125,"occurences":23},"for":{"probability":0.03125,"occurences":23}},"massive":{"attack":{"probability":1,"occurences":1}},"date":{"men":{"probability":0.33333333333333,"occurences":3},"as":{"probability":0.33333333333333,"occurences":3},"again":{"probability":0.33333333333333,"occurences":3}},"same":{"thing?":{"probability":0.25,"occurences":4},"rules.":{"probability":0.25,"occurences":4},"thing":{"probability":0.5,"occurences":8}},"happy,":{"and":{"probability":1,"occurences":1}},"new":{"each":{"probability":0.33333333333333,"occurences":1},"age":{"probability":0.33333333333333,"occurences":1},"she":{"probability":0.33333333333333,"occurences":1}},"you'll":{"find":{"probability":1,"occurences":1}},"hard":{"to":{"probability":0.71428571428571,"occurences":35},"on":{"probability":0.14285714285714,"occurences":7},"it":{"probability":0.14285714285714,"occurences":7}},"what's":{"happening":{"probability":0.5,"occurences":2},"frustrating.":{"probability":0.5,"occurences":2}},"scared":{"to":{"probability":0.33333333333333,"occurences":3},"that":{"probability":0.66666666666667,"occurences":6}},"sex.":{"I'm":{"probability":1,"occurences":1}},"it,":{"and":{"probability":0.5,"occurences":1},"makes":{"probability":0.5,"occurences":1}},"achieve,":{"you":{"probability":1,"occurences":1}},"watch":{"the":{"probability":1,"occurences":1}},"nothing":{"for":{"probability":1,"occurences":1}},"were":{"the":{"probability":0.14285714285714,"occurences":7},"attached":{"probability":0.14285714285714,"occurences":7},"best":{"probability":0.14285714285714,"occurences":7},"designed":{"probability":0.14285714285714,"occurences":7},"always":{"probability":0.14285714285714,"occurences":7},"a":{"probability":0.14285714285714,"occurences":7},"going":{"probability":0.14285714285714,"occurences":7}},"trip":{"for":{"probability":1,"occurences":1}},"thoughtful":{"than":{"probability":1,"occurences":1}},"legs":{"were":{"probability":1,"occurences":1}},"like":{"to":{"probability":0.1875,"occurences":48},"shit,":{"probability":0.0625,"occurences":16},"we're":{"probability":0.0625,"occurences":16},"gold,":{"probability":0.0625,"occurences":16},"I've":{"probability":0.0625,"occurences":16},"I":{"probability":0.125,"occurences":32},"gold;":{"probability":0.0625,"occurences":16},"there":{"probability":0.0625,"occurences":16},"it":{"probability":0.0625,"occurences":16},"a":{"probability":0.1875,"occurences":48},"this":{"probability":0.0625,"occurences":16}},"horrible":{"relationship":{"probability":0.5,"occurences":2},"friend.":{"probability":0.5,"occurences":2}},"anymore.":{"I":{"probability":1,"occurences":16}},"wooden":{"shield":{"probability":1,"occurences":1}},"bit":{"skeptical":{"probability":0.5,"occurences":1},"of":{"probability":0.5,"occurences":1}},"that.":{"The":{"probability":0.2,"occurences":5},"I":{"probability":0.4,"occurences":10},"He":{"probability":0.2,"occurences":5},"I've":{"probability":0.2,"occurences":5}},"Christian,":{"a":{"probability":0.5,"occurences":2},"I":{"probability":0.5,"occurences":2}},"very":{"straightforward,":{"probability":0.14285714285714,"occurences":7},"much":{"probability":0.14285714285714,"occurences":7},"basic":{"probability":0.14285714285714,"occurences":7},"honest":{"probability":0.14285714285714,"occurences":7},"detailed":{"probability":0.14285714285714,"occurences":7},"pessimistic":{"probability":0.14285714285714,"occurences":7},"heavy":{"probability":0.14285714285714,"occurences":7}},"stop":{"harassing":{"probability":0.2,"occurences":5},"comparing":{"probability":0.4,"occurences":10},"being":{"probability":0.2,"occurences":5},"to":{"probability":0.2,"occurences":5}},"human":{"being":{"probability":1,"occurences":1}},"Hell.":{"I":{"probability":1,"occurences":25}},"be":{"grateful":{"probability":0.035087719298246,"occurences":114},"friends":{"probability":0.017543859649123,"occurences":57},"so":{"probability":0.017543859649123,"occurences":57},"able":{"probability":0.035087719298246,"occurences":114},"doing":{"probability":0.035087719298246,"occurences":114},"grateful.":{"probability":0.017543859649123,"occurences":57},"impossible":{"probability":0.017543859649123,"occurences":57},"harder":{"probability":0.017543859649123,"occurences":57},"saved,":{"probability":0.017543859649123,"occurences":57},"reached.":{"probability":0.017543859649123,"occurences":57},"free.":{"probability":0.017543859649123,"occurences":57},"saved.":{"probability":0.052631578947368,"occurences":171},"the":{"probability":0.017543859649123,"occurences":57},"weary":{"probability":0.017543859649123,"occurences":57},"sued":{"probability":0.017543859649123,"occurences":57},"great.":{"probability":0.017543859649123,"occurences":57},"with":{"probability":0.017543859649123,"occurences":57},"better":{"probability":0.017543859649123,"occurences":57},"carrying":{"probability":0.017543859649123,"occurences":57},"good":{"probability":0.017543859649123,"occurences":57},"either":{"probability":0.035087719298246,"occurences":114},"a":{"probability":0.47368421052632,"occurences":1539},"Christian":{"probability":0.052631578947368,"occurences":171}},"basic":{"description":{"probability":1,"occurences":1}},"head":{"to":{"probability":0.5,"occurences":2},"full":{"probability":0.5,"occurences":2}},"he":{"blog":{"probability":0.0057803468208092,"occurences":16},"'tumblrite'":{"probability":0.0057803468208092,"occurences":16},"creators":{"probability":0.0057803468208092,"occurences":16},"horse":{"probability":0.0057803468208092,"occurences":16},"devil,":{"probability":0.0057803468208092,"occurences":16},"says,":{"probability":0.0057803468208092,"occurences":16},"first":{"probability":0.0057803468208092,"occurences":16},"metal":{"probability":0.0057803468208092,"occurences":16},"saddle":{"probability":0.0057803468208092,"occurences":16},"source":{"probability":0.0057803468208092,"occurences":16},"time.":{"probability":0.0057803468208092,"occurences":16},"time":{"probability":0.023121387283237,"occurences":64},"bad":{"probability":0.0057803468208092,"occurences":16},"tells":{"probability":0.0057803468208092,"occurences":16},"realizes":{"probability":0.0057803468208092,"occurences":16},"had":{"probability":0.0057803468208092,"occurences":16},"fan.":{"probability":0.0057803468208092,"occurences":16},"Ninth":{"probability":0.0057803468208092,"occurences":16},"reason":{"probability":0.0057803468208092,"occurences":16},"lives":{"probability":0.0057803468208092,"occurences":16},"benefit":{"probability":0.0057803468208092,"occurences":16},"front":{"probability":0.0057803468208092,"occurences":16},"quote":{"probability":0.011560693641618,"occurences":32},"plaintiff.":{"probability":0.0057803468208092,"occurences":16},"Tumblr":{"probability":0.011560693641618,"occurences":32},"fuck?":{"probability":0.0057803468208092,"occurences":16},"wanted":{"probability":0.011560693641618,"occurences":32},"was":{"probability":0.040462427745665,"occurences":112},"could":{"probability":0.0057803468208092,"occurences":16},"enemy.":{"probability":0.0057803468208092,"occurences":16},"problems,":{"probability":0.0057803468208092,"occurences":16},"enemy's":{"probability":0.0057803468208092,"occurences":16},"'tumbl'":{"probability":0.0057803468208092,"occurences":16},"only":{"probability":0.017341040462428,"occurences":48},"fuck":{"probability":0.0057803468208092,"occurences":16},"case.":{"probability":0.011560693641618,"occurences":32},"case":{"probability":0.017341040462428,"occurences":48},"problems.":{"probability":0.0057803468208092,"occurences":16},"guy":{"probability":0.0057803468208092,"occurences":16},"court":{"probability":0.0057803468208092,"occurences":16},"\"unfortunate":{"probability":0.0057803468208092,"occurences":16},"people":{"probability":0.0057803468208092,"occurences":16},"gratitude":{"probability":0.0057803468208092,"occurences":16},"fan,":{"probability":0.0057803468208092,"occurences":16},"next":{"probability":0.0057803468208092,"occurences":16},"Bible":{"probability":0.0057803468208092,"occurences":16},"can":{"probability":0.0057803468208092,"occurences":16},"fan":{"probability":0.017341040462428,"occurences":48},"career":{"probability":0.0057803468208092,"occurences":16},"rider":{"probability":0.011560693641618,"occurences":32},"did":{"probability":0.011560693641618,"occurences":32},"type":{"probability":0.0057803468208092,"occurences":16},"loved.":{"probability":0.0057803468208092,"occurences":16},"plates":{"probability":0.0057803468208092,"occurences":16},"suit":{"probability":0.011560693641618,"occurences":32},"same":{"probability":0.017341040462428,"occurences":48},"fan;":{"probability":0.0057803468208092,"occurences":16},"tumblrites":{"probability":0.011560693641618,"occurences":32},"best":{"probability":0.023121387283237,"occurences":64},"leg.":{"probability":0.0057803468208092,"occurences":16},"ones":{"probability":0.0057803468208092,"occurences":16},"journey":{"probability":0.0057803468208092,"occurences":16},"has":{"probability":0.0057803468208092,"occurences":16},"point":{"probability":0.011560693641618,"occurences":32},"enemy":{"probability":0.0057803468208092,"occurences":16},"second":{"probability":0.0057803468208092,"occurences":16},"site,":{"probability":0.0057803468208092,"occurences":16},"told":{"probability":0.0057803468208092,"occurences":16},"word":{"probability":0.0057803468208092,"occurences":16},"medieval":{"probability":0.0057803468208092,"occurences":16},"topic,":{"probability":0.0057803468208092,"occurences":16},"lawsuit":{"probability":0.0057803468208092,"occurences":16},"use":{"probability":0.0057803468208092,"occurences":16},"whole":{"probability":0.0057803468208092,"occurences":16},"sole":{"probability":0.011560693641618,"occurences":32},"legs":{"probability":0.0057803468208092,"occurences":16},"fight":{"probability":0.011560693641618,"occurences":32},"plaintiff":{"probability":0.0057803468208092,"occurences":16},"open":{"probability":0.0057803468208092,"occurences":16},"person":{"probability":0.011560693641618,"occurences":32},"knights":{"probability":0.0057803468208092,"occurences":16},"government":{"probability":0.011560693641618,"occurences":32},"plaintiff's":{"probability":0.0057803468208092,"occurences":16},"verses":{"probability":0.0057803468208092,"occurences":16},"envelope":{"probability":0.0057803468208092,"occurences":16},"most":{"probability":0.017341040462428,"occurences":48},"works":{"probability":0.0057803468208092,"occurences":16},"right":{"probability":0.011560693641618,"occurences":32},"ordinary.":{"probability":0.0057803468208092,"occurences":16},"problem":{"probability":0.0057803468208092,"occurences":16},"claim":{"probability":0.0057803468208092,"occurences":16},"still":{"probability":0.0057803468208092,"occurences":16},"just":{"probability":0.0057803468208092,"occurences":16},"armor":{"probability":0.011560693641618,"occurences":32},"consequences":{"probability":0.0057803468208092,"occurences":16},"habit":{"probability":0.0057803468208092,"occurences":16},"women":{"probability":0.0057803468208092,"occurences":16},"third":{"probability":0.0057803468208092,"occurences":16},"claims":{"probability":0.0057803468208092,"occurences":16},"must":{"probability":0.0057803468208092,"occurences":16},"world":{"probability":0.011560693641618,"occurences":32},"opportunity,":{"probability":0.011560693641618,"occurences":32},"things":{"probability":0.011560693641618,"occurences":32},"say":{"probability":0.0057803468208092,"occurences":16},"treats":{"probability":0.0057803468208092,"occurences":16},"one":{"probability":0.011560693641618,"occurences":32},"would":{"probability":0.011560693641618,"occurences":32},"should":{"probability":0.011560693641618,"occurences":32},"equivalent":{"probability":0.0057803468208092,"occurences":16},"basis":{"probability":0.0057803468208092,"occurences":16},"graduated":{"probability":0.0057803468208092,"occurences":16},"exact":{"probability":0.0057803468208092,"occurences":16},"company.":{"probability":0.0057803468208092,"occurences":16},"'tumblrites'":{"probability":0.040462427745665,"occurences":112},"kind":{"probability":0.0057803468208092,"occurences":16},"man":{"probability":0.0057803468208092,"occurences":16},"said":{"probability":0.0057803468208092,"occurences":16},"end":{"probability":0.0057803468208092,"occurences":16},"king":{"probability":0.034682080924855,"occurences":96}},"husband":{"and":{"probability":0.66666666666667,"occurences":6},"before":{"probability":0.33333333333333,"occurences":3}},"harassing":{"them.":{"probability":0.5,"occurences":2},"and":{"probability":0.5,"occurences":2}},"treats":{"us":{"probability":1,"occurences":1}},"gloves,":{"and":{"probability":1,"occurences":1}},"Circuit":{"and":{"probability":1,"occurences":1}},"vote":{"on":{"probability":1,"occurences":1}},"California":{"federal":{"probability":1,"occurences":1}},"defend":{"his":{"probability":0.5,"occurences":2},"it":{"probability":0.5,"occurences":2}},"company.":{"It's":{"probability":0.5,"occurences":2},"She":{"probability":0.5,"occurences":2}},"free.":{"How":{"probability":1,"occurences":1}},"pretty":{"well-known":{"probability":1,"occurences":1}},"back?":{"Do":{"probability":1,"occurences":1}},"grateful":{"for":{"probability":1,"occurences":9}},"also":{"don't":{"probability":0.25,"occurences":4},"ask":{"probability":0.25,"occurences":4},"a":{"probability":0.25,"occurences":4},"know":{"probability":0.25,"occurences":4}},"else":{"is.":{"probability":1,"occurences":1}},"see":{"no":{"probability":0.25,"occurences":4},"her":{"probability":0.25,"occurences":4},"her.":{"probability":0.25,"occurences":4},"this":{"probability":0.25,"occurences":4}},"plaintiff's":{"lawyer":{"probability":1,"occurences":1}},"story.":{"So":{"probability":1,"occurences":1}},"He":{"would":{"probability":0.125,"occurences":8},"told":{"probability":0.125,"occurences":8},"was":{"probability":0.5,"occurences":32},"wasn't":{"probability":0.125,"occurences":8},"got":{"probability":0.125,"occurences":8}},"gentleman.":{"He":{"probability":1,"occurences":1}},"games":{"anymore,":{"probability":1,"occurences":1}},"much.":{"I":{"probability":1,"occurences":1}},"short":{"spear":{"probability":0.5,"occurences":2},"sword":{"probability":0.5,"occurences":2}},"computer":{"techs.":{"probability":0.5,"occurences":2},"or":{"probability":0.5,"occurences":2}},"working":{"3":{"probability":0.33333333333333,"occurences":3},"my":{"probability":0.33333333333333,"occurences":3},"two":{"probability":0.33333333333333,"occurences":3}},"than":{"their":{"probability":0.33333333333333,"occurences":3},"when":{"probability":0.66666666666667,"occurences":6}},"made":{"of":{"probability":0.33333333333333,"occurences":3},"me":{"probability":0.33333333333333,"occurences":3},"my":{"probability":0.33333333333333,"occurences":3}},"jobs":{"and":{"probability":0.5,"occurences":2},"just":{"probability":0.5,"occurences":2}},"wish":{"you":{"probability":0.33333333333333,"occurences":1},"or":{"probability":0.66666666666667,"occurences":2}},"child":{"with":{"probability":0.5,"occurences":2},"because":{"probability":0.5,"occurences":2}},"000.":{"The":{"probability":1,"occurences":1}},"bringing":{"the":{"probability":1,"occurences":1}},"lose":{"my":{"probability":0.5,"occurences":2},"weight":{"probability":0.5,"occurences":2}},"sucks.":{"I'm":{"probability":1,"occurences":1}},"about":{"to":{"probability":0.090909090909091,"occurences":11},"women":{"probability":0.090909090909091,"occurences":11},"the":{"probability":0.18181818181818,"occurences":22},"it,":{"probability":0.090909090909091,"occurences":11},"what":{"probability":0.090909090909091,"occurences":11},"everything.":{"probability":0.090909090909091,"occurences":11},"it.":{"probability":0.090909090909091,"occurences":11},"taking":{"probability":0.090909090909091,"occurences":11},"getting":{"probability":0.090909090909091,"occurences":11},"this":{"probability":0.090909090909091,"occurences":11}},"matters.":{"I":{"probability":1,"occurences":1}},"I'm":{"trying":{"probability":0.02,"occurences":50},"in":{"probability":0.04,"occurences":100},"just":{"probability":0.04,"occurences":100},"so":{"probability":0.06,"occurences":150},"not":{"probability":0.16,"occurences":400},"doing":{"probability":0.04,"occurences":100},"an":{"probability":0.02,"occurences":50},"sorry":{"probability":0.06,"occurences":150},"super":{"probability":0.02,"occurences":50},"doing.":{"probability":0.02,"occurences":50},"fairly":{"probability":0.02,"occurences":50},"promoting":{"probability":0.02,"occurences":50},"going":{"probability":0.06,"occurences":150},"talking":{"probability":0.02,"occurences":50},"16":{"probability":0.02,"occurences":50},"sure":{"probability":0.02,"occurences":50},"really":{"probability":0.06,"occurences":150},"getting":{"probability":0.02,"occurences":50},"dating":{"probability":0.06,"occurences":150},"a":{"probability":0.16,"occurences":400},"scared":{"probability":0.06,"occurences":150}},"but":{"it's":{"probability":0.037037037037037,"occurences":27},"a":{"probability":0.037037037037037,"occurences":27},"be":{"probability":0.037037037037037,"occurences":27},"are":{"probability":0.037037037037037,"occurences":27},"on":{"probability":0.074074074074074,"occurences":54},"I'm":{"probability":0.14814814814815,"occurences":108},"the":{"probability":0.074074074074074,"occurences":54},"he":{"probability":0.037037037037037,"occurences":27},"if":{"probability":0.074074074074074,"occurences":54},"it":{"probability":0.11111111111111,"occurences":81},"sometimes":{"probability":0.037037037037037,"occurences":27},"there's":{"probability":0.037037037037037,"occurences":27},"they":{"probability":0.11111111111111,"occurences":81},"I":{"probability":0.14814814814815,"occurences":108}},"store.":{"It":{"probability":1,"occurences":1}},"useless":{"to":{"probability":1,"occurences":1}},"boy":{"with":{"probability":1,"occurences":4}},"sure":{"you":{"probability":0.25,"occurences":4},"that":{"probability":0.25,"occurences":4},"what's":{"probability":0.25,"occurences":4},"she":{"probability":0.25,"occurences":4}},"shit,":{"makes":{"probability":1,"occurences":1}},"will":{"for":{"probability":0.071428571428571,"occurences":14},"and":{"probability":0.071428571428571,"occurences":14},"be":{"probability":0.21428571428571,"occurences":42},"see":{"probability":0.071428571428571,"occurences":14},"save":{"probability":0.14285714285714,"occurences":28},"he":{"probability":0.14285714285714,"occurences":28},"that":{"probability":0.071428571428571,"occurences":14},"feel":{"probability":0.071428571428571,"occurences":14},"also":{"probability":0.071428571428571,"occurences":14},"alone.":{"probability":0.071428571428571,"occurences":14}},"spoiled":{"brat.":{"probability":1,"occurences":1}},"difficult.":{"She":{"probability":1,"occurences":2}},"works":{"with":{"probability":1,"occurences":1}},"It's":{"about":{"probability":0.083333333333333,"occurences":12},"\"oh,":{"probability":0.083333333333333,"occurences":12},"like":{"probability":0.083333333333333,"occurences":12},"hard":{"probability":0.083333333333333,"occurences":12},"not":{"probability":0.083333333333333,"occurences":12},"worth":{"probability":0.083333333333333,"occurences":12},"really":{"probability":0.083333333333333,"occurences":12},"just":{"probability":0.16666666666667,"occurences":24},"a":{"probability":0.16666666666667,"occurences":24},"easy":{"probability":0.083333333333333,"occurences":12}},"best":{"religion":{"probability":0.125,"occurences":8},"that":{"probability":0.125,"occurences":8},"and":{"probability":0.125,"occurences":8},"of":{"probability":0.125,"occurences":8},"friends":{"probability":0.25,"occurences":16},"friend's":{"probability":0.125,"occurences":8},"to":{"probability":0.125,"occurences":8}},"years,":{"and":{"probability":1,"occurences":1}},"lives":{"of":{"probability":1,"occurences":1}},"be.":{"People":{"probability":1,"occurences":1}},"with.":{"But":{"probability":1,"occurences":1}},"the":{"'tumblrite'":{"probability":0.0084745762711864,"occurences":118},"creators":{"probability":0.0084745762711864,"occurences":118},"horse":{"probability":0.0084745762711864,"occurences":118},"devil,":{"probability":0.0084745762711864,"occurences":118},"basis":{"probability":0.0084745762711864,"occurences":118},"metal":{"probability":0.0084745762711864,"occurences":118},"saddle":{"probability":0.0084745762711864,"occurences":118},"source":{"probability":0.0084745762711864,"occurences":118},"time.":{"probability":0.0084745762711864,"occurences":118},"time":{"probability":0.033898305084746,"occurences":472},"bad":{"probability":0.0084745762711864,"occurences":118},"fan.":{"probability":0.0084745762711864,"occurences":118},"Ninth":{"probability":0.0084745762711864,"occurences":118},"reason":{"probability":0.0084745762711864,"occurences":118},"lives":{"probability":0.0084745762711864,"occurences":118},"Bible":{"probability":0.0084745762711864,"occurences":118},"front":{"probability":0.0084745762711864,"occurences":118},"plaintiff.":{"probability":0.0084745762711864,"occurences":118},"Tumblr":{"probability":0.0084745762711864,"occurences":118},"fuck?":{"probability":0.0084745762711864,"occurences":118},"enemy":{"probability":0.0084745762711864,"occurences":118},"enemy.":{"probability":0.0084745762711864,"occurences":118},"rider":{"probability":0.016949152542373,"occurences":236},"only":{"probability":0.016949152542373,"occurences":236},"habit":{"probability":0.0084745762711864,"occurences":118},"problems.":{"probability":0.0084745762711864,"occurences":118},"\"unfortunate":{"probability":0.0084745762711864,"occurences":118},"people":{"probability":0.0084745762711864,"occurences":118},"gratitude":{"probability":0.0084745762711864,"occurences":118},"next":{"probability":0.0084745762711864,"occurences":118},"career":{"probability":0.0084745762711864,"occurences":118},"court":{"probability":0.0084745762711864,"occurences":118},"type":{"probability":0.0084745762711864,"occurences":118},"case.":{"probability":0.016949152542373,"occurences":236},"women":{"probability":0.0084745762711864,"occurences":118},"leg.":{"probability":0.0084745762711864,"occurences":118},"second":{"probability":0.0084745762711864,"occurences":118},"word":{"probability":0.0084745762711864,"occurences":118},"medieval":{"probability":0.0084745762711864,"occurences":118},"use":{"probability":0.0084745762711864,"occurences":118},"tumblrites":{"probability":0.016949152542373,"occurences":236},"sole":{"probability":0.016949152542373,"occurences":236},"legs":{"probability":0.0084745762711864,"occurences":118},"topic,":{"probability":0.0084745762711864,"occurences":118},"person":{"probability":0.016949152542373,"occurences":236},"king":{"probability":0.042372881355932,"occurences":590},"knights":{"probability":0.0084745762711864,"occurences":118},"claims":{"probability":0.0084745762711864,"occurences":118},"best":{"probability":0.033898305084746,"occurences":472},"verses":{"probability":0.0084745762711864,"occurences":118},"envelope":{"probability":0.0084745762711864,"occurences":118},"whole":{"probability":0.0084745762711864,"occurences":118},"'tumbl'":{"probability":0.0084745762711864,"occurences":118},"right":{"probability":0.016949152542373,"occurences":236},"ordinary.":{"probability":0.0084745762711864,"occurences":118},"point":{"probability":0.016949152542373,"occurences":236},"problems,":{"probability":0.0084745762711864,"occurences":118},"fan,":{"probability":0.0084745762711864,"occurences":118},"government":{"probability":0.016949152542373,"occurences":236},"armor":{"probability":0.016949152542373,"occurences":236},"open":{"probability":0.0084745762711864,"occurences":118},"case":{"probability":0.016949152542373,"occurences":236},"guy":{"probability":0.0084745762711864,"occurences":118},"quote":{"probability":0.016949152542373,"occurences":236},"consequences":{"probability":0.0084745762711864,"occurences":118},"ones":{"probability":0.0084745762711864,"occurences":118},"world":{"probability":0.016949152542373,"occurences":236},"opportunity,":{"probability":0.016949152542373,"occurences":236},"things":{"probability":0.016949152542373,"occurences":236},"enemy's":{"probability":0.0084745762711864,"occurences":118},"fuck":{"probability":0.0084745762711864,"occurences":118},"one":{"probability":0.016949152542373,"occurences":236},"site,":{"probability":0.0084745762711864,"occurences":118},"fan;":{"probability":0.0084745762711864,"occurences":118},"equivalent":{"probability":0.0084745762711864,"occurences":118},"same":{"probability":0.025423728813559,"occurences":354},"benefit":{"probability":0.0084745762711864,"occurences":118},"claim":{"probability":0.0084745762711864,"occurences":118},"company.":{"probability":0.0084745762711864,"occurences":118},"'tumblrites'":{"probability":0.033898305084746,"occurences":472},"kind":{"probability":0.0084745762711864,"occurences":118},"man":{"probability":0.0084745762711864,"occurences":118},"exact":{"probability":0.0084745762711864,"occurences":118},"end":{"probability":0.0084745762711864,"occurences":118},"most":{"probability":0.025423728813559,"occurences":354}},"planned":{"a":{"probability":1,"occurences":1}},"military":{"and":{"probability":1,"occurences":1}},"bodyguard":{"then":{"probability":0.33333333333333,"occurences":3},"is":{"probability":0.66666666666667,"occurences":6}},"appreciative":{"of":{"probability":1,"occurences":1}},"deliberately":{"causing":{"probability":1,"occurences":1}},"part,":{"the":{"probability":1,"occurences":1}},"coworker,":{"and":{"probability":1,"occurences":1}},"frustrating":{"to":{"probability":1,"occurences":1}},"suit":{"was":{"probability":0.33333333333333,"occurences":2},"alleges":{"probability":0.33333333333333,"occurences":2},"is":{"probability":0.33333333333333,"occurences":2}},"school.":{"Why":{"probability":1,"occurences":1}},"harder.":{"It's":{"probability":1,"occurences":1}},"better.":{"I":{"probability":1,"occurences":1}},"here":{"who":{"probability":0.055555555555556,"occurences":2},"are":{"probability":0.22222222222222,"occurences":8},"is":{"probability":0.11111111111111,"occurences":4},"it":{"probability":0.055555555555556,"occurences":2},"I":{"probability":0.055555555555556,"occurences":2},"to":{"probability":0.16666666666667,"occurences":6},"will":{"probability":0.11111111111111,"occurences":4},"I'm":{"probability":0.11111111111111,"occurences":4},"everything's":{"probability":0.055555555555556,"occurences":2},"and":{"probability":0.055555555555556,"occurences":2}},"tired":{"but":{"probability":1,"occurences":1}},"study":{"and":{"probability":1,"occurences":1}},"But":{"I":{"probability":0.375,"occurences":24},"that's":{"probability":0.125,"occurences":8},"if":{"probability":0.375,"occurences":24},"it":{"probability":0.125,"occurences":8}},"manager":{"about":{"probability":1,"occurences":1}},"frustrating.":{"I've":{"probability":1,"occurences":1}},"benefit":{"is":{"probability":1,"occurences":1}},"That's":{"what":{"probability":0.33333333333333,"occurences":3},"my":{"probability":0.33333333333333,"occurences":3},"so":{"probability":0.33333333333333,"occurences":3}},"him":{"anymore.":{"probability":0.11111111111111,"occurences":9},"not":{"probability":0.11111111111111,"occurences":9},"because":{"probability":0.11111111111111,"occurences":9},"defend":{"probability":0.11111111111111,"occurences":9},"go":{"probability":0.33333333333333,"occurences":27},"make":{"probability":0.22222222222222,"occurences":18}},"My":{"husband":{"probability":1,"occurences":1}},"promotion":{"when":{"probability":1,"occurences":1}},"crush":{"on":{"probability":1,"occurences":4}},"choose":{"the":{"probability":0.5,"occurences":2},"his":{"probability":0.5,"occurences":2}},"species,":{"and":{"probability":1,"occurences":1}},"fired":{"or":{"probability":1,"occurences":1}},"promotion.":{"What's":{"probability":1,"occurences":1}},"Thank":{"you":{"probability":1,"occurences":1}},"happy":{"for":{"probability":0.66666666666667,"occurences":6},"that":{"probability":0.33333333333333,"occurences":3}},"seen.":{"I'm":{"probability":1,"occurences":1}},"The":{"lawsuit":{"probability":0.047619047619048,"occurences":21},"suit":{"probability":0.095238095238095,"occurences":42},"plates":{"probability":0.047619047619048,"occurences":21},"fan":{"probability":0.14285714285714,"occurences":63},"journey":{"probability":0.047619047619048,"occurences":21},"Tumblr":{"probability":0.047619047619048,"occurences":21},"plaintiff":{"probability":0.047619047619048,"occurences":21},"plaintiff's":{"probability":0.047619047619048,"occurences":21},"third":{"probability":0.047619047619048,"occurences":21},"problem":{"probability":0.047619047619048,"occurences":21},"only":{"probability":0.047619047619048,"occurences":21},"first":{"probability":0.047619047619048,"occurences":21},"'tumblrites'":{"probability":0.14285714285714,"occurences":63},"case":{"probability":0.047619047619048,"occurences":21},"blog":{"probability":0.047619047619048,"occurences":21},"king":{"probability":0.047619047619048,"occurences":21}},"relation":{"to":{"probability":1,"occurences":1}},"if":{"you":{"probability":0.40909090909091,"occurences":198},"the":{"probability":0.13636363636364,"occurences":66},"I":{"probability":0.045454545454545,"occurences":22},"they":{"probability":0.090909090909091,"occurences":44},"a":{"probability":0.31818181818182,"occurences":154}},"Yeah":{"I":{"probability":1,"occurences":1}},"of":{"enemy,":{"probability":0.017857142857143,"occurences":56},"women":{"probability":0.017857142857143,"occurences":56},"everyone":{"probability":0.035714285714286,"occurences":112},"posting":{"probability":0.035714285714286,"occurences":112},"the":{"probability":0.17857142857143,"occurences":560},"wearing":{"probability":0.017857142857143,"occurences":56},"an":{"probability":0.017857142857143,"occurences":56},"being":{"probability":0.035714285714286,"occurences":112},"people.":{"probability":0.017857142857143,"occurences":56},"person":{"probability":0.017857142857143,"occurences":56},"computer":{"probability":0.017857142857143,"occurences":56},"self-proclaimed":{"probability":0.017857142857143,"occurences":56},"crazy,":{"probability":0.017857142857143,"occurences":56},"life.":{"probability":0.035714285714286,"occurences":112},"us":{"probability":0.017857142857143,"occurences":56},"$15":{"probability":0.017857142857143,"occurences":56},"bed":{"probability":0.017857142857143,"occurences":56},"years":{"probability":0.017857142857143,"occurences":56},"bringing":{"probability":0.017857142857143,"occurences":56},"blogs,":{"probability":0.035714285714286,"occurences":112},"of":{"probability":0.017857142857143,"occurences":56},"problems.":{"probability":0.017857142857143,"occurences":56},"luck.":{"probability":0.017857142857143,"occurences":56},"people":{"probability":0.053571428571429,"occurences":168},"mail":{"probability":0.017857142857143,"occurences":56},"me":{"probability":0.017857142857143,"occurences":56},"it":{"probability":0.017857142857143,"occurences":56},"enemy":{"probability":0.017857142857143,"occurences":56},"a":{"probability":0.125,"occurences":392},"doing":{"probability":0.017857142857143,"occurences":56},"money":{"probability":0.017857142857143,"occurences":56},"you":{"probability":0.017857142857143,"occurences":56},"my":{"probability":0.017857142857143,"occurences":56},"harassment.":{"probability":0.017857142857143,"occurences":56}},"mother,":{"so":{"probability":1,"occurences":1}},"We're":{"just":{"probability":1,"occurences":1}},"remain":{"on":{"probability":1,"occurences":1}},"steel;":{"let":{"probability":1,"occurences":1}},"career":{"of":{"probability":1,"occurences":1}},"don't":{"stop.":{"probability":0.027027027027027,"occurences":37},"have":{"probability":0.054054054054054,"occurences":74},"believe":{"probability":0.21621621621622,"occurences":296},"like":{"probability":0.027027027027027,"occurences":37},"care":{"probability":0.027027027027027,"occurences":37},"need":{"probability":0.18918918918919,"occurences":259},"even":{"probability":0.10810810810811,"occurences":148},"know":{"probability":0.16216216216216,"occurences":222},"deal":{"probability":0.054054054054054,"occurences":74},"think":{"probability":0.081081081081081,"occurences":111},"want":{"probability":0.054054054054054,"occurences":74}},"other":{"than":{"probability":0.25,"occurences":4},"very":{"probability":0.25,"occurences":4},"so":{"probability":0.5,"occurences":8}},"step":{"in":{"probability":1,"occurences":1}},"money":{"is":{"probability":0.5,"occurences":2},"and":{"probability":0.5,"occurences":2}},"move":{"up":{"probability":1,"occurences":1}},"chance":{"for":{"probability":1,"occurences":1}},"changed":{"your":{"probability":0.5,"occurences":2},"mine.":{"probability":0.5,"occurences":2}},"going.":{"I've":{"probability":1,"occurences":1}},"gold;":{"let":{"probability":1,"occurences":1}},"mature":{"and":{"probability":1,"occurences":1}},"man":{"who":{"probability":0.4,"occurences":6},"for":{"probability":0.2,"occurences":3},"with":{"probability":0.2,"occurences":3},"being":{"probability":0.2,"occurences":3}},"can't":{"help":{"probability":0.25,"occurences":4},"fucking":{"probability":0.25,"occurences":4},"do":{"probability":0.25,"occurences":4},"even":{"probability":0.25,"occurences":4}},"involved":{"thinks":{"probability":1,"occurences":1}},"king":{"about":{"probability":0.029411764705882,"occurences":15},"child":{"probability":0.029411764705882,"occurences":15},"excited":{"probability":0.029411764705882,"occurences":15},"kid?":{"probability":0.029411764705882,"occurences":15},"has":{"probability":0.14705882352941,"occurences":75},"the":{"probability":0.088235294117647,"occurences":45},"happy":{"probability":0.029411764705882,"occurences":15},"stand":{"probability":0.029411764705882,"occurences":15},"videos,":{"probability":0.029411764705882,"occurences":15},"for":{"probability":0.058823529411765,"occurences":30},"about.":{"probability":0.029411764705882,"occurences":15},"must":{"probability":0.14705882352941,"occurences":75},"is":{"probability":0.088235294117647,"occurences":45},"two":{"probability":0.029411764705882,"occurences":15},"kid.":{"probability":0.029411764705882,"occurences":15},"3":{"probability":0.029411764705882,"occurences":15},"my":{"probability":0.029411764705882,"occurences":15},"was":{"probability":0.029411764705882,"occurences":15},"went":{"probability":0.029411764705882,"occurences":15},"idiot.":{"probability":0.029411764705882,"occurences":15},"together.":{"probability":0.029411764705882,"occurences":15}},"visit":{"the":{"probability":0.5,"occurences":2},"for":{"probability":0.5,"occurences":2}},"frustrating,":{"because":{"probability":1,"occurences":1}},"stronger":{"and":{"probability":1,"occurences":1}},"which,":{"if":{"probability":1,"occurences":1}},"in":{"their":{"probability":0.016393442622951,"occurences":57},"about":{"probability":0.016393442622951,"occurences":57},"are":{"probability":0.016393442622951,"occurences":57},"armor":{"probability":0.016393442622951,"occurences":57},"the":{"probability":0.081967213114754,"occurences":285},"relation":{"probability":0.016393442622951,"occurences":57},"her":{"probability":0.016393442622951,"occurences":57},"arms,":{"probability":0.016393442622951,"occurences":57},"battle.":{"probability":0.016393442622951,"occurences":57},"medieval":{"probability":0.016393442622951,"occurences":57},"this":{"probability":0.032786885245902,"occurences":114},"order":{"probability":0.016393442622951,"occurences":57},"a":{"probability":0.065573770491803,"occurences":228},"God,":{"probability":0.016393442622951,"occurences":57},"grade":{"probability":0.016393442622951,"occurences":57},"on":{"probability":0.016393442622951,"occurences":57},"engineering":{"probability":0.016393442622951,"occurences":57},"steel;":{"probability":0.016393442622951,"occurences":57},"silence.":{"probability":0.016393442622951,"occurences":57},"mail,":{"probability":0.016393442622951,"occurences":57},"abusive":{"probability":0.016393442622951,"occurences":57},"Jesus":{"probability":0.016393442622951,"occurences":57},"life.":{"probability":0.016393442622951,"occurences":57},"front":{"probability":0.016393442622951,"occurences":57},"question":{"probability":0.049180327868852,"occurences":171},"my":{"probability":0.016393442622951,"occurences":57},"God":{"probability":0.40983606557377,"occurences":1425}},"listen":{"to":{"probability":1,"occurences":4}},"on":{"where":{"probability":0.023255813953488,"occurences":17},"have":{"probability":0.023255813953488,"occurences":17},"about":{"probability":0.023255813953488,"occurences":17},"my":{"probability":0.023255813953488,"occurences":17},"horseback,":{"probability":0.046511627906977,"occurences":34},"who":{"probability":0.069767441860465,"occurences":51},"her":{"probability":0.046511627906977,"occurences":34},"me.":{"probability":0.023255813953488,"occurences":17},"of":{"probability":0.11627906976744,"occurences":85},"by":{"probability":0.023255813953488,"occurences":17},"what":{"probability":0.023255813953488,"occurences":17},"and":{"probability":0.046511627906977,"occurences":34},"any":{"probability":0.023255813953488,"occurences":17},"were":{"probability":0.023255813953488,"occurences":17},"facebook,":{"probability":0.023255813953488,"occurences":17},"or":{"probability":0.023255813953488,"occurences":17},"was":{"probability":0.023255813953488,"occurences":17},"posts.":{"probability":0.023255813953488,"occurences":17},"is":{"probability":0.046511627906977,"occurences":34},"reddit's":{"probability":0.023255813953488,"occurences":17},"I":{"probability":0.046511627906977,"occurences":34},"when":{"probability":0.023255813953488,"occurences":17},"but":{"probability":0.023255813953488,"occurences":17},"she":{"probability":0.023255813953488,"occurences":17},"to":{"probability":0.069767441860465,"occurences":51},"yourself.":{"probability":0.023255813953488,"occurences":17},"a":{"probability":0.023255813953488,"occurences":17},"the":{"probability":0.069767441860465,"occurences":51}},"I've":{"told":{"probability":0.14285714285714,"occurences":7},"just":{"probability":0.14285714285714,"occurences":7},"been":{"probability":0.71428571428571,"occurences":35}},"gets":{"really":{"probability":1,"occurences":1}},"later":{"went":{"probability":1,"occurences":1}},"abusive":{"relationships.":{"probability":1,"occurences":1}},"Because":{"they":{"probability":0.5,"occurences":2},"she's":{"probability":0.5,"occurences":2}},"others":{"and":{"probability":1,"occurences":1}},"sometimes":{"it's":{"probability":1,"occurences":1}},"grateful.":{"I":{"probability":1,"occurences":1}},"easy":{"to":{"probability":1,"occurences":1}},"everything.":{"It's":{"probability":1,"occurences":1}},"8":{"years.":{"probability":1,"occurences":1}},"complain":{"about":{"probability":1,"occurences":1}},"constantly":{"complain":{"probability":1,"occurences":1}},"gratitude":{"with":{"probability":1,"occurences":1}},"felt":{"like":{"probability":1,"occurences":9}},"things,":{"but":{"probability":1,"occurences":1}},"him!\"":{"It's":{"probability":1,"occurences":1}},"Ninth":{"Circuit":{"probability":1,"occurences":1}},"amazing":{"person.":{"probability":0.5,"occurences":2},"human":{"probability":0.5,"occurences":2}},"outs":{"with":{"probability":1,"occurences":1}},"Bible":{"in":{"probability":0.33333333333333,"occurences":3},"is":{"probability":0.33333333333333,"occurences":3},"passage":{"probability":0.33333333333333,"occurences":3}},"angry?":{"People":{"probability":1,"occurences":1}},"TIL":{"about":{"probability":1,"occurences":1}},"time,":{"but":{"probability":1,"occurences":1}},"brat.":{"I":{"probability":1,"occurences":1}},"act":{"same":{"probability":0.5,"occurences":1},"like":{"probability":0.5,"occurences":1}},"knight's":{"equipment":{"probability":1,"occurences":1}},"anything":{"when":{"probability":1,"occurences":1}},"turn":{"around":{"probability":1,"occurences":1}},"In":{"the":{"probability":1,"occurences":1}},"purpose":{"of":{"probability":1,"occurences":4}},"own":{"for":{"probability":0.33333333333333,"occurences":1},"as":{"probability":0.33333333333333,"occurences":1},"and":{"probability":0.33333333333333,"occurences":1}},"dismissed":{"by":{"probability":1,"occurences":1}},"description":{"of":{"probability":1,"occurences":4}},"heart":{"into":{"probability":1,"occurences":1}},"put":{"your":{"probability":1,"occurences":1}},"God,":{"you":{"probability":1,"occurences":1}},"could":{"learn":{"probability":0.33333333333333,"occurences":3},"spend":{"probability":0.33333333333333,"occurences":3},"never":{"probability":0.33333333333333,"occurences":3}},"case":{"was":{"probability":0.25,"occurences":4},"tomorrow":{"probability":0.25,"occurences":4},"of":{"probability":0.25,"occurences":4},"has":{"probability":0.25,"occurences":4}},"person,":{"you're":{"probability":0.5,"occurences":2},"why":{"probability":0.5,"occurences":2}},"kid,":{"you're":{"probability":1,"occurences":1}},"saved,":{"I":{"probability":1,"occurences":1}},"\"unfortunate":{"accident\"":{"probability":1,"occurences":1}},"You're":{"not":{"probability":1,"occurences":1}},"big":{"fan":{"probability":0.5,"occurences":2},"guy":{"probability":0.5,"occurences":2}},"funny.":{"I'm":{"probability":1,"occurences":1}},"they're":{"working":{"probability":0.5,"occurences":2},"so":{"probability":0.5,"occurences":2}},"less.":{"For":{"probability":0.33333333333333,"occurences":1},"That's":{"probability":0.33333333333333,"occurences":1},"Now":{"probability":0.33333333333333,"occurences":1}},"out,":{"and":{"probability":0.5,"occurences":2},"but":{"probability":0.5,"occurences":2}},"greedy":{"child":{"probability":1,"occurences":1}},"How":{"do":{"probability":1,"occurences":1}},"accident\"":{"of":{"probability":1,"occurences":1}},"Now":{"to":{"probability":1,"occurences":1}},"type":{"of":{"probability":1,"occurences":4}},"pony.":{"The":{"probability":1,"occurences":1}},"looking":{"for":{"probability":1,"occurences":4}},"telling":{"her":{"probability":1,"occurences":1}},"character":{"in":{"probability":1,"occurences":1}},"pay":{"their":{"probability":1,"occurences":1}},"enemy,":{"but":{"probability":1,"occurences":1}},"problem":{"is":{"probability":1,"occurences":1}},"promoting":{"is":{"probability":1,"occurences":1}},"ecstatic":{"that":{"probability":1,"occurences":1}},"\"oh,":{"I'm":{"probability":1,"occurences":4}},"has":{"to":{"probability":0.625,"occurences":40},"been":{"probability":0.125,"occurences":8},"a":{"probability":0.125,"occurences":8},"made":{"probability":0.125,"occurences":8}},"parents":{"about":{"probability":1,"occurences":1}},"left":{"her":{"probability":1,"occurences":1}},"doing":{"and":{"probability":0.1,"occurences":10},"this":{"probability":0.1,"occurences":10},"so?":{"probability":0.1,"occurences":10},"with":{"probability":0.1,"occurences":10},"the":{"probability":0.1,"occurences":10},"more":{"probability":0.1,"occurences":10},"what":{"probability":0.1,"occurences":10},"something":{"probability":0.1,"occurences":10},"my":{"probability":0.1,"occurences":10},"things":{"probability":0.1,"occurences":10}},"because":{"of":{"probability":0.11111111111111,"occurences":9},"I":{"probability":0.22222222222222,"occurences":18},"everyone":{"probability":0.22222222222222,"occurences":18},"I'm":{"probability":0.44444444444444,"occurences":36}},"particular":{"time,":{"probability":1,"occurences":1}},"weary":{"of":{"probability":1,"occurences":1}},"husband?":{"What":{"probability":1,"occurences":1}},"videos,":{"I":{"probability":1,"occurences":1}},"lawsuit":{"alleges":{"probability":1,"occurences":1}},"default,":{"but":{"probability":1,"occurences":1}},"car":{"or":{"probability":1,"occurences":1}},"promoted.":{"I":{"probability":1,"occurences":1}},"situation":{"where":{"probability":1,"occurences":1}},"your":{"worth.":{"probability":0.125,"occurences":8},"heart":{"probability":0.125,"occurences":8},"view.":{"probability":0.125,"occurences":8},"view":{"probability":0.125,"occurences":8},"potential.":{"probability":0.125,"occurences":8},"job.":{"probability":0.125,"occurences":8},"life":{"probability":0.125,"occurences":8},"past.":{"probability":0.125,"occurences":8}},"exact":{"same":{"probability":1,"occurences":1}},"two":{"years,":{"probability":0.5,"occurences":2},"jobs":{"probability":0.5,"occurences":2}},"above,":{"the":{"probability":1,"occurences":1}},"similar":{"situation":{"probability":1,"occurences":1}},"over":{"their":{"probability":0.5,"occurences":2},"8":{"probability":0.5,"occurences":2}},"information":{"about":{"probability":1,"occurences":1}},"guy,":{"but":{"probability":1,"occurences":1}},"age":{"of":{"probability":0.25,"occurences":1},"more":{"probability":0.25,"occurences":1},"you":{"probability":0.25,"occurences":1},"or":{"probability":0.25,"occurences":1}},"wasn't":{"a":{"probability":1,"occurences":1}},"well-known":{"as":{"probability":1,"occurences":1}},"suing":{"the":{"probability":0.5,"occurences":2},"for":{"probability":0.5,"occurences":2}},"tumblr":{"specific":{"probability":1,"occurences":4}},"armor,":{"his":{"probability":1,"occurences":1}},"appealed":{"to":{"probability":1,"occurences":1}},"problems.":{"I":{"probability":0.5,"occurences":2},"So,":{"probability":0.5,"occurences":2}},"parent,":{"or":{"probability":1,"occurences":1}},"Hell":{"or":{"probability":1,"occurences":1}},"problems,":{"and":{"probability":1,"occurences":1}},"excited.":{"So,":{"probability":1,"occurences":1}},"heavy":{"armor":{"probability":0.5,"occurences":2},"that":{"probability":0.5,"occurences":2}},"pony,":{"a":{"probability":0.5,"occurences":2},"and":{"probability":0.5,"occurences":2}},"fuck":{"is":{"probability":1,"occurences":1}},"said,":{"\"What":{"probability":1,"occurences":1}},"from":{"falling":{"probability":0.33333333333333,"occurences":3},"reaching":{"probability":0.33333333333333,"occurences":3},"the":{"probability":0.33333333333333,"occurences":3}},"discourage":{"you":{"probability":1,"occurences":1}},"divorced.":{"The":{"probability":1,"occurences":1}},"reaching":{"out":{"probability":1,"occurences":1}},"thing?":{"Then":{"probability":1,"occurences":1}},"one":{"to":{"probability":0.076923076923077,"occurences":3},"she":{"probability":0.076923076923077,"occurences":3},"else":{"probability":0.076923076923077,"occurences":3},"on":{"probability":0.076923076923077,"occurences":3},"I":{"probability":0.076923076923077,"occurences":3},"who":{"probability":0.15384615384615,"occurences":6},"needs":{"probability":0.23076923076923,"occurences":9},"with":{"probability":0.15384615384615,"occurences":6},"and":{"probability":0.076923076923077,"occurences":3}},"would":{"typically":{"probability":0.071428571428571,"occurences":14},"have":{"probability":0.21428571428571,"occurences":42},"be":{"probability":0.28571428571429,"occurences":56},"argue":{"probability":0.071428571428571,"occurences":14},"then":{"probability":0.071428571428571,"occurences":14},"generally":{"probability":0.071428571428571,"occurences":14},"probably":{"probability":0.071428571428571,"occurences":14},"she":{"probability":0.071428571428571,"occurences":14},"still":{"probability":0.071428571428571,"occurences":14}},"fuck?":{"What":{"probability":1,"occurences":1}},"kid?":{"What":{"probability":1,"occurences":1}},"thankful.":{"Why":{"probability":1,"occurences":1}},"lost":{"and":{"probability":0.5,"occurences":2},"his":{"probability":0.5,"occurences":2}},"It":{"makes":{"probability":1,"occurences":1}},"her,":{"and":{"probability":0.5,"occurences":1},"so":{"probability":0.5,"occurences":1}},"candy":{"store.":{"probability":1,"occurences":1}},"kid":{"in":{"probability":1,"occurences":1}},"try":{"to":{"probability":1,"occurences":1}},"harassing.":{"This":{"probability":1,"occurences":1}},"sat":{"around":{"probability":1,"occurences":1}},"ass":{"off,":{"probability":1,"occurences":1}},"beautiful.":{"You":{"probability":1,"occurences":1}},"'tumblrite'":{"is":{"probability":1,"occurences":1}},"asshole":{"and":{"probability":1,"occurences":1}},"excited":{"to":{"probability":1,"occurences":1}},"job.":{"I":{"probability":1,"occurences":1}},"guy":{"but":{"probability":0.33333333333333,"occurences":3},"and":{"probability":0.33333333333333,"occurences":3},"who":{"probability":0.33333333333333,"occurences":3}},"ago.":{"The":{"probability":1,"occurences":1}},"tend":{"to":{"probability":1,"occurences":1}},"no":{"friends,":{"probability":0.2,"occurences":5},"reason":{"probability":0.2,"occurences":5},"one":{"probability":0.2,"occurences":5},"real":{"probability":0.2,"occurences":5},"need":{"probability":0.2,"occurences":5}},"too.":{"It's":{"probability":1,"occurences":1}},"take":{"care":{"probability":0.66666666666667,"occurences":6},"it.":{"probability":0.33333333333333,"occurences":3}},"total":{"asshole":{"probability":1,"occurences":1}},"selfish,":{"fucking":{"probability":1,"occurences":4}},"mobile":{"phone":{"probability":1,"occurences":1}},"idiot.":{"I":{"probability":1,"occurences":1}},"--":{"but":{"probability":1,"occurences":1}},"fucking":{"kid.":{"probability":0.14285714285714,"occurences":7},"kid?":{"probability":0.14285714285714,"occurences":7},"happy":{"probability":0.14285714285714,"occurences":7},"stand":{"probability":0.14285714285714,"occurences":7},"excited":{"probability":0.14285714285714,"occurences":7},"idiot.":{"probability":0.14285714285714,"occurences":7},"child":{"probability":0.14285714285714,"occurences":7}},"together.":{"I":{"probability":1,"occurences":4}},"do":{"this":{"probability":0.090909090909091,"occurences":44},"anything":{"probability":0.045454545454545,"occurences":22},"or":{"probability":0.045454545454545,"occurences":22},"I":{"probability":0.045454545454545,"occurences":22},"it.":{"probability":0.090909090909091,"occurences":44},"the":{"probability":0.090909090909091,"occurences":44},"it":{"probability":0.045454545454545,"occurences":22},"anymore.":{"probability":0.045454545454545,"occurences":22},"to":{"probability":0.045454545454545,"occurences":22},"things":{"probability":0.045454545454545,"occurences":22},"that.":{"probability":0.090909090909091,"occurences":44},"what":{"probability":0.090909090909091,"occurences":44},"and":{"probability":0.045454545454545,"occurences":22},"with":{"probability":0.13636363636364,"occurences":66},"nothing":{"probability":0.045454545454545,"occurences":22}},"blogger":{"who":{"probability":1,"occurences":1}},"conspiracy":{"theorists,":{"probability":1,"occurences":1}},"raise":{"is":{"probability":1,"occurences":1}},"third":{"part":{"probability":1,"occurences":1}},"Christian":{"or":{"probability":1,"occurences":16}},"pace":{"with":{"probability":1,"occurences":1}},"self-proclaimed":{"'tumblrites'":{"probability":1,"occurences":1}},"post":{"to":{"probability":0.5,"occurences":2},"tomorrow":{"probability":0.5,"occurences":2}},"really":{"not":{"probability":0.28571428571429,"occurences":14},"appreciate":{"probability":0.14285714285714,"occurences":7},"want":{"probability":0.14285714285714,"occurences":7},"interesting.":{"probability":0.14285714285714,"occurences":7},"scared.":{"probability":0.14285714285714,"occurences":7},"frustrating,":{"probability":0.14285714285714,"occurences":7}},"it.\"":{"So":{"probability":1,"occurences":1}},"raise,":{"and":{"probability":0.5,"occurences":2},"this":{"probability":0.5,"occurences":2}},"distress\"":{"to":{"probability":1,"occurences":1}},"\"If":{"you're":{"probability":1,"occurences":1}},"little":{"and":{"probability":0.5,"occurences":2},"older":{"probability":0.5,"occurences":2}},"Tumblr":{"in":{"probability":0.16666666666667,"occurences":6},"called":{"probability":0.16666666666667,"occurences":6},"community":{"probability":0.33333333333333,"occurences":12},"is":{"probability":0.33333333333333,"occurences":12}},"to":{"work.":{"probability":0.0045454545454545,"occurences":215},"battle,":{"probability":0.0045454545454545,"occurences":215},"re-file":{"probability":0.0045454545454545,"occurences":215},"see":{"probability":0.0090909090909091,"occurences":430},"save":{"probability":0.0090909090909091,"occurences":430},"achieve":{"probability":0.0045454545454545,"occurences":215},"understand":{"probability":0.0045454545454545,"occurences":215},"these":{"probability":0.0045454545454545,"occurences":215},"cry":{"probability":0.0090909090909091,"occurences":430},"each":{"probability":0.0045454545454545,"occurences":215},"keep":{"probability":0.0090909090909091,"occurences":430},"go":{"probability":0.036363636363636,"occurences":1720},"do.":{"probability":0.013636363636364,"occurences":645},"changing":{"probability":0.0045454545454545,"occurences":215},"know":{"probability":0.036363636363636,"occurences":1720},"call":{"probability":0.0045454545454545,"occurences":215},"come":{"probability":0.0090909090909091,"occurences":430},"work":{"probability":0.013636363636364,"occurences":645},"help":{"probability":0.0090909090909091,"occurences":430},"pray":{"probability":0.0090909090909091,"occurences":430},"her":{"probability":0.0090909090909091,"occurences":430},"it.":{"probability":0.0045454545454545,"occurences":215},"work,":{"probability":0.0090909090909091,"occurences":430},"change":{"probability":0.0090909090909091,"occurences":430},"lose":{"probability":0.0045454545454545,"occurences":215},"others.":{"probability":0.0045454545454545,"occurences":215},"make":{"probability":0.031818181818182,"occurences":1505},"date":{"probability":0.0090909090909091,"occurences":430},"my":{"probability":0.018181818181818,"occurences":860},"women":{"probability":0.0045454545454545,"occurences":215},"read":{"probability":0.018181818181818,"occurences":860},"the":{"probability":0.031818181818182,"occurences":1505},"steal":{"probability":0.0045454545454545,"occurences":215},"me.":{"probability":0.0090909090909091,"occurences":430},"achieve,":{"probability":0.0045454545454545,"occurences":215},"watch":{"probability":0.0045454545454545,"occurences":215},"this":{"probability":0.0045454545454545,"occurences":215},"school.":{"probability":0.0045454545454545,"occurences":215},"explain":{"probability":0.0045454545454545,"occurences":215},"exercise.":{"probability":0.0045454545454545,"occurences":215},"follow":{"probability":0.0045454545454545,"occurences":215},"stop":{"probability":0.022727272727273,"occurences":1075},"go.":{"probability":0.0045454545454545,"occurences":215},"Hell.":{"probability":0.022727272727273,"occurences":1075},"learn":{"probability":0.0090909090909091,"occurences":430},"be":{"probability":0.19090909090909,"occurences":9030},"talk":{"probability":0.0045454545454545,"occurences":215},"Hell":{"probability":0.0045454545454545,"occurences":215},"open":{"probability":0.0045454545454545,"occurences":215},"battle.":{"probability":0.0045454545454545,"occurences":215},"mail.":{"probability":0.0090909090909091,"occurences":430},"provide":{"probability":0.0090909090909091,"occurences":430},"take":{"probability":0.013636363636364,"occurences":645},"believe":{"probability":0.081818181818182,"occurences":3870},"find":{"probability":0.022727272727273,"occurences":1075},"discourage":{"probability":0.0045454545454545,"occurences":215},"do":{"probability":0.077272727272727,"occurences":3655},"have":{"probability":0.0045454545454545,"occurences":215},"eat.":{"probability":0.0045454545454545,"occurences":215},"a":{"probability":0.013636363636364,"occurences":645},"pay":{"probability":0.0045454545454545,"occurences":215},"you.":{"probability":0.0045454545454545,"occurences":215},"vote":{"probability":0.0045454545454545,"occurences":215},"become":{"probability":0.0045454545454545,"occurences":215},"but":{"probability":0.0045454545454545,"occurences":215},"move":{"probability":0.0045454545454545,"occurences":215},"her,":{"probability":0.0045454545454545,"occurences":215},"clarify,":{"probability":0.0045454545454545,"occurences":215},"walk":{"probability":0.0045454545454545,"occurences":215},"try":{"probability":0.0045454545454545,"occurences":215},"wear":{"probability":0.0045454545454545,"occurences":215},"figure":{"probability":0.0090909090909091,"occurences":430},"get":{"probability":0.027272727272727,"occurences":1290}},"She":{"said":{"probability":0.2,"occurences":5},"wanted":{"probability":0.2,"occurences":5},"graduated":{"probability":0.2,"occurences":5},"treats":{"probability":0.2,"occurences":5},"has":{"probability":0.2,"occurences":5}},"media,":{"and":{"probability":1,"occurences":4}},"envelope":{"and":{"probability":0.5,"occurences":2},"full":{"probability":0.5,"occurences":2}},"large":{"envelope":{"probability":1,"occurences":1}},"need":{"to.":{"probability":0.022727272727273,"occurences":44},"to":{"probability":0.95454545454545,"occurences":1848},"answers.":{"probability":0.022727272727273,"occurences":44}},"hands":{"me":{"probability":1,"occurences":1}},"social":{"media,":{"probability":1,"occurences":4}},"her":{"and":{"probability":0.15625,"occurences":115},"without":{"probability":0.03125,"occurences":23},"again.":{"probability":0.03125,"occurences":23},"so":{"probability":0.09375,"occurences":69},"the":{"probability":0.03125,"occurences":23},"there":{"probability":0.03125,"occurences":23},"as":{"probability":0.0625,"occurences":46},"record":{"probability":0.03125,"occurences":23},"dad.":{"probability":0.03125,"occurences":23},"sad":{"probability":0.03125,"occurences":23},"videos":{"probability":0.03125,"occurences":23},"looks":{"probability":0.03125,"occurences":23},"to":{"probability":0.09375,"occurences":69},"birthday.":{"probability":0.03125,"occurences":23},"place":{"probability":0.03125,"occurences":23},"very":{"probability":0.03125,"occurences":23},"than":{"probability":0.03125,"occurences":23},"out,":{"probability":0.03125,"occurences":23},"fault":{"probability":0.03125,"occurences":23},"older":{"probability":0.03125,"occurences":23},"a":{"probability":0.0625,"occurences":46},"but":{"probability":0.03125,"occurences":23}},"men,":{"but":{"probability":1,"occurences":1}},"battles.":{"But":{"probability":1,"occurences":1}},"today":{"and":{"probability":1,"occurences":1}},"as":{"also":{"probability":0.02,"occurences":10},"well,":{"probability":0.04,"occurences":20},"in":{"probability":0.04,"occurences":20},"first":{"probability":0.02,"occurences":10},"created":{"probability":0.02,"occurences":10},"not":{"probability":0.02,"occurences":10},"20,":{"probability":0.02,"occurences":10},"crying.":{"probability":0.02,"occurences":10},"filed":{"probability":0.02,"occurences":10},"I":{"probability":0.02,"occurences":10},"working":{"probability":0.02,"occurences":10},"long":{"probability":0.02,"occurences":10},"very":{"probability":0.02,"occurences":10},"made":{"probability":0.02,"occurences":10},"to":{"probability":0.1,"occurences":50},"just":{"probability":0.06,"occurences":30},"so":{"probability":0.02,"occurences":10},"basically":{"probability":0.02,"occurences":10},"her":{"probability":0.02,"occurences":10},"happy":{"probability":0.02,"occurences":10},"always":{"probability":0.04,"occurences":20},"they":{"probability":0.02,"occurences":10},"we":{"probability":0.02,"occurences":10},"granted":{"probability":0.02,"occurences":10},"heartbroken.":{"probability":0.02,"occurences":10},"asked":{"probability":0.02,"occurences":10},"going":{"probability":0.02,"occurences":10},"been":{"probability":0.02,"occurences":10},"the":{"probability":0.06,"occurences":30},"everything":{"probability":0.02,"occurences":10},"dismissed":{"probability":0.02,"occurences":10},"designed":{"probability":0.02,"occurences":10},"excited.":{"probability":0.02,"occurences":10},"a":{"probability":0.1,"occurences":50},"hurting.":{"probability":0.02,"occurences":10}},"sad.":{"The":{"probability":1,"occurences":1}},"calling":{"her":{"probability":0.5,"occurences":2},"for":{"probability":0.5,"occurences":2}},"man.":{"He":{"probability":1,"occurences":2}},"Do":{"you":{"probability":1,"occurences":1}},"others.":{"I":{"probability":1,"occurences":1}},"boss's":{"house":{"probability":1,"occurences":1}},"next":{"day.":{"probability":1,"occurences":1}},"A":{"king,":{"probability":0.5,"occurences":2},"lot":{"probability":0.5,"occurences":2}},"father":{"and":{"probability":0.5,"occurences":2},"looks":{"probability":0.5,"occurences":2}},"did":{"this.":{"probability":1,"occurences":4}},"argue":{"that":{"probability":1,"occurences":1}},"plaintiff":{"will":{"probability":1,"occurences":1}},"my":{"claim":{"probability":0.037037037037037,"occurences":25},"best":{"probability":0.074074074074074,"occurences":50},"life":{"probability":0.14814814814815,"occurences":100},"own":{"probability":0.037037037037037,"occurences":25},"the":{"probability":0.037037037037037,"occurences":25},"story.":{"probability":0.037037037037037,"occurences":25},"job,":{"probability":0.037037037037037,"occurences":25},"fate.":{"probability":0.037037037037037,"occurences":25},"boss's":{"probability":0.037037037037037,"occurences":25},"manager":{"probability":0.037037037037037,"occurences":25},"house":{"probability":0.037037037037037,"occurences":25},"point":{"probability":0.037037037037037,"occurences":25},"life,":{"probability":0.037037037037037,"occurences":25},"boss":{"probability":0.074074074074074,"occurences":50},"coworkers.":{"probability":0.037037037037037,"occurences":25},"company.":{"probability":0.037037037037037,"occurences":25},"life.":{"probability":0.037037037037037,"occurences":25},"ass":{"probability":0.037037037037037,"occurences":25},"formation":{"probability":0.037037037037037,"occurences":25},"will":{"probability":0.074074074074074,"occurences":50},"parents":{"probability":0.037037037037037,"occurences":25}},"'tumblr')":{"related":{"probability":1,"occurences":4}},"giving":{"me":{"probability":1,"occurences":4}},"she's":{"basically":{"probability":0.25,"occurences":4},"giving":{"probability":0.5,"occurences":8},"so":{"probability":0.25,"occurences":4}},"got":{"divorced.":{"probability":0.2,"occurences":5},"to":{"probability":0.2,"occurences":5},"a":{"probability":0.4,"occurences":10},"this":{"probability":0.2,"occurences":5}},"real":{"discussion":{"probability":1,"occurences":1}},"harder":{"to":{"probability":1,"occurences":1}},"came":{"to":{"probability":0.5,"occurences":1},"best":{"probability":0.5,"occurences":1}},"job":{"for":{"probability":0.33333333333333,"occurences":3},"that":{"probability":0.33333333333333,"occurences":3},"because":{"probability":0.33333333333333,"occurences":3}},"read":{"the":{"probability":0.2,"occurences":5},"them":{"probability":0.2,"occurences":5},"this.":{"probability":0.2,"occurences":5},"a":{"probability":0.2,"occurences":5},"it":{"probability":0.2,"occurences":5}},"yesterday":{"my":{"probability":1,"occurences":1}},"posting":{"'tumbl'":{"probability":1,"occurences":4}},"learn.":{"I":{"probability":1,"occurences":1}},"What's":{"the":{"probability":1,"occurences":1}},"arms":{"but":{"probability":0.33333333333333,"occurences":3},"so":{"probability":0.66666666666667,"occurences":6}},"steal":{"their":{"probability":1,"occurences":1}},"hurting":{"her":{"probability":1,"occurences":1}},"happy.":{"You":{"probability":1,"occurences":1}},"never":{"asked":{"probability":0.5,"occurences":2},"talk":{"probability":0.5,"occurences":2}},"being":{"the":{"probability":0.16666666666667,"occurences":6},"that":{"probability":0.16666666666667,"occurences":6},"married":{"probability":0.16666666666667,"occurences":6},"a":{"probability":0.33333333333333,"occurences":12},"so":{"probability":0.16666666666667,"occurences":6}},"fighting":{"at":{"probability":1,"occurences":1}},"people.":{"And":{"probability":1,"occurences":1}},"day.":{"I":{"probability":1,"occurences":2}},"whole":{"journey":{"probability":1,"occurences":1}},"us":{"feel":{"probability":0.16666666666667,"occurences":4},"or":{"probability":0.16666666666667,"occurences":4},"talking":{"probability":0.16666666666667,"occurences":4},"like":{"probability":0.16666666666667,"occurences":4},"costs":{"probability":0.16666666666667,"occurences":4},"think":{"probability":0.16666666666667,"occurences":4}},"fault":{"and":{"probability":1,"occurences":1}},"silence.":{"I":{"probability":1,"occurences":1}},"around":{"in":{"probability":0.5,"occurences":2},"and":{"probability":0.5,"occurences":2}},"cried.":{"We":{"probability":1,"occurences":1}},"I":{"wanted":{"probability":0.0062893081761006,"occurences":159},"have":{"probability":0.050314465408805,"occurences":1272},"believe":{"probability":0.0062893081761006,"occurences":159},"feel":{"probability":0.018867924528302,"occurences":477},"left":{"probability":0.0062893081761006,"occurences":159},"can":{"probability":0.0062893081761006,"occurences":159},"am":{"probability":0.012578616352201,"occurences":318},"started":{"probability":0.0062893081761006,"occurences":159},"saw":{"probability":0.0062893081761006,"occurences":159},"go":{"probability":0.0062893081761006,"occurences":159},"was":{"probability":0.056603773584906,"occurences":1431},"had":{"probability":0.0062893081761006,"occurences":159},"thought":{"probability":0.012578616352201,"occurences":318},"never":{"probability":0.0062893081761006,"occurences":159},"didn't":{"probability":0.037735849056604,"occurences":954},"mean":{"probability":0.0062893081761006,"occurences":159},"really":{"probability":0.012578616352201,"occurences":318},"think,":{"probability":0.0062893081761006,"occurences":159},"actually":{"probability":0.0062893081761006,"occurences":159},"dated":{"probability":0.0062893081761006,"occurences":159},"still":{"probability":0.0062893081761006,"occurences":159},"can't":{"probability":0.025157232704403,"occurences":636},"see":{"probability":0.0062893081761006,"occurences":159},"read":{"probability":0.0062893081761006,"occurences":159},"wish":{"probability":0.0062893081761006,"occurences":159},"felt":{"probability":0.018867924528302,"occurences":477},"just":{"probability":0.012578616352201,"occurences":318},"could":{"probability":0.012578616352201,"occurences":318},"open":{"probability":0.0062893081761006,"occurences":159},"work":{"probability":0.012578616352201,"occurences":318},"haven't":{"probability":0.0062893081761006,"occurences":159},"will":{"probability":0.025157232704403,"occurences":636},"think":{"probability":0.025157232704403,"occurences":636},"need":{"probability":0.012578616352201,"occurences":318},"always":{"probability":0.012578616352201,"occurences":318},"met":{"probability":0.0062893081761006,"occurences":159},"went":{"probability":0.012578616352201,"occurences":318},"get":{"probability":0.025157232704403,"occurences":636},"also":{"probability":0.012578616352201,"occurences":318},"don't":{"probability":0.13207547169811,"occurences":3339},"would":{"probability":0.025157232704403,"occurences":636},"should":{"probability":0.0062893081761006,"occurences":159},"love":{"probability":0.031446540880503,"occurences":795},"knew":{"probability":0.0062893081761006,"occurences":159},"want":{"probability":0.0062893081761006,"occurences":159},"changed":{"probability":0.0062893081761006,"occurences":159},"cried":{"probability":0.012578616352201,"occurences":318},"know":{"probability":0.018867924528302,"occurences":477},"told":{"probability":0.0062893081761006,"occurences":159},"hate":{"probability":0.19496855345912,"occurences":4929},"ever":{"probability":0.0062893081761006,"occurences":159},"missed":{"probability":0.0062893081761006,"occurences":159},"got":{"probability":0.0062893081761006,"occurences":159}},"knights":{"plate":{"probability":1,"occurences":1}},"equipment":{"and":{"probability":1,"occurences":1}},"hurting.":{"I":{"probability":1,"occurences":1}},"sad":{"and":{"probability":1,"occurences":1}},"call":{"her":{"probability":1,"occurences":4}},"crying.":{"I":{"probability":1,"occurences":1}},"still":{"committed":{"probability":0.125,"occurences":8},"go":{"probability":0.125,"occurences":8},"have":{"probability":0.125,"occurences":8},"in":{"probability":0.125,"occurences":8},"love":{"probability":0.25,"occurences":16},"dating":{"probability":0.125,"occurences":8},"makes":{"probability":0.125,"occurences":8}},"right":{"type":{"probability":0.33333333333333,"occurences":2},"infringement.":{"probability":0.33333333333333,"occurences":2},"armor":{"probability":0.33333333333333,"occurences":2}},"his":{"raise":{"probability":0.025641025641026,"occurences":22},"and":{"probability":0.025641025641026,"occurences":22},"armor":{"probability":0.28205128205128,"occurences":242},"past":{"probability":0.025641025641026,"occurences":22},"bodyguard":{"probability":0.051282051282051,"occurences":44},"battles.":{"probability":0.025641025641026,"occurences":22},"company,":{"probability":0.025641025641026,"occurences":22},"despite":{"probability":0.025641025641026,"occurences":22},"mail":{"probability":0.025641025641026,"occurences":22},"or":{"probability":0.025641025641026,"occurences":22},"girl":{"probability":0.025641025641026,"occurences":22},"mind":{"probability":0.025641025641026,"occurences":22},"job":{"probability":0.051282051282051,"occurences":44},"but":{"probability":0.025641025641026,"occurences":22},"mail.":{"probability":0.076923076923077,"occurences":66},"post":{"probability":0.025641025641026,"occurences":22},"is":{"probability":0.12820512820513,"occurences":110},"country;":{"probability":0.025641025641026,"occurences":22},"sub":{"probability":0.025641025641026,"occurences":22},"mail,":{"probability":0.025641025641026,"occurences":22},"alone.":{"probability":0.025641025641026,"occurences":22}},"cry":{"and":{"probability":1,"occurences":4}},"me.":{"I":{"probability":0.6,"occurences":12},"I'm":{"probability":0.4,"occurences":8}},"divorced":{"and":{"probability":1,"occurences":1}},"talk":{"to":{"probability":0.66666666666667,"occurences":6},"to.":{"probability":0.33333333333333,"occurences":3}},"journey":{"and":{"probability":0.5,"occurences":2},"to":{"probability":0.5,"occurences":2}},"knight":{"in":{"probability":0.5,"occurences":2},"wears":{"probability":0.5,"occurences":2}},"missed":{"her":{"probability":0.5,"occurences":1},"by":{"probability":0.5,"occurences":1}},"known":{"as":{"probability":0.5,"occurences":1},"and":{"probability":0.5,"occurences":1}},"attitude,":{"but":{"probability":1,"occurences":1}},"typically":{"go":{"probability":1,"occurences":1}},"heartbroken.":{"We":{"probability":1,"occurences":1}},"always":{"together.":{"probability":0.2,"occurences":5},"wanted":{"probability":0.2,"occurences":5},"had":{"probability":0.2,"occurences":5},"a":{"probability":0.2,"occurences":5},"open":{"probability":0.2,"occurences":5}},"back":{"with":{"probability":1,"occurences":1}},"dad.":{"She":{"probability":1,"occurences":1}},"content.":{"The":{"probability":0.25,"occurences":4},"Tumblr":{"probability":0.5,"occurences":8},"It's":{"probability":0.25,"occurences":4}},"birthday.":{"I":{"probability":1,"occurences":1}},"People":{"are":{"probability":0.33333333333333,"occurences":3},"who":{"probability":0.33333333333333,"occurences":3},"have":{"probability":0.33333333333333,"occurences":3}},"attack":{"against":{"probability":1,"occurences":1}},"should":{"choose":{"probability":0.125,"occurences":8},"have":{"probability":0.125,"occurences":8},"be":{"probability":0.375,"occurences":24},"go":{"probability":0.125,"occurences":8},"save":{"probability":0.125,"occurences":8},"probably":{"probability":0.125,"occurences":8}},"term":{"used":{"probability":1,"occurences":1}},"dating":{"him!":{"probability":0.11111111111111,"occurences":9},"him!\"":{"probability":0.11111111111111,"occurences":9},"him":{"probability":0.11111111111111,"occurences":9},"him.\"":{"probability":0.11111111111111,"occurences":9},"are":{"probability":0.11111111111111,"occurences":9},"a":{"probability":0.22222222222222,"occurences":18},"married":{"probability":0.22222222222222,"occurences":18}},"meeting.":{"The":{"probability":1,"occurences":1}},"relationships.":{"I":{"probability":1,"occurences":1}},"coworkers.":{"I":{"probability":1,"occurences":1}},"worth":{"a":{"probability":0.5,"occurences":2},"pointing":{"probability":0.5,"occurences":2}},"basis":{"that":{"probability":1,"occurences":1}},"2":{"years":{"probability":1,"occurences":1}},"So,":{"she's":{"probability":0.25,"occurences":4},"you're":{"probability":0.25,"occurences":4},"I'm":{"probability":0.5,"occurences":8}},"clarify,":{"a":{"probability":1,"occurences":1}},"this.":{"I":{"probability":0.6,"occurences":15},"Because":{"probability":0.2,"occurences":5},"So":{"probability":0.2,"occurences":5}},"became":{"best":{"probability":1,"occurences":1}},"His":{"armor":{"probability":1,"occurences":1}},"$4,000":{"raise.":{"probability":1,"occurences":1}},"trying":{"to":{"probability":1,"occurences":4}},"anybody":{"else.":{"probability":1,"occurences":1}},"damn.":{"I've":{"probability":1,"occurences":1}},"says,":{"\"If":{"probability":1,"occurences":1}},"well.":{"I":{"probability":1,"occurences":1}},"wanted":{"in":{"probability":0.2,"occurences":5},"me":{"probability":0.2,"occurences":5},"and":{"probability":0.2,"occurences":5},"to":{"probability":0.4,"occurences":10}},"everything":{"I":{"probability":1,"occurences":1}},"theorists,":{"but":{"probability":1,"occurences":1}},"claim":{"that":{"probability":1,"occurences":4}},"started":{"to":{"probability":1,"occurences":1}},"us,":{"must":{"probability":1,"occurences":1}},"We":{"saw":{"probability":0.25,"occurences":4},"weren't":{"probability":0.25,"occurences":4},"sat":{"probability":0.25,"occurences":4},"were":{"probability":0.25,"occurences":4}},"years.":{"We":{"probability":1,"occurences":1}},"9":{"and":{"probability":1,"occurences":1}},"making":{"videos,":{"probability":1,"occurences":1}},"facebook,":{"then":{"probability":1,"occurences":1}},"love.":{"You":{"probability":0.5,"occurences":2},"I'm":{"probability":0.5,"occurences":2}},"want":{"to":{"probability":0.75,"occurences":108},"me":{"probability":0.083333333333333,"occurences":12},"her":{"probability":0.083333333333333,"occurences":12},"out":{"probability":0.083333333333333,"occurences":12}},"changing":{"his":{"probability":1,"occurences":1}},"money,":{"you":{"probability":0.5,"occurences":2},"money":{"probability":0.5,"occurences":2}},"all":{"day":{"probability":0.14285714285714,"occurences":4},"ages'":{"probability":0.14285714285714,"occurences":4},"her":{"probability":0.28571428571429,"occurences":8},"the":{"probability":0.14285714285714,"occurences":4},"about":{"probability":0.14285714285714,"occurences":4},"that":{"probability":0.14285714285714,"occurences":4}},"specific":{"content.":{"probability":1,"occurences":4}},"steady":{"and":{"probability":1,"occurences":1}},"mean":{"that":{"probability":0.33333333333333,"occurences":3},"by":{"probability":0.33333333333333,"occurences":3},"his":{"probability":0.33333333333333,"occurences":3}},"horse":{"and":{"probability":1,"occurences":1}},"celebrity":{"for":{"probability":1,"occurences":1}},"plate":{"which":{"probability":0.5,"occurences":2},"was":{"probability":0.5,"occurences":2}},"single":{"mother,":{"probability":1,"occurences":1}},"those":{"women":{"probability":0.5,"occurences":2},"kind":{"probability":0.5,"occurences":2}},"support":{"for":{"probability":1,"occurences":1}},"ask":{"me":{"probability":0.5,"occurences":2},"for":{"probability":0.5,"occurences":2}},"who":{"made":{"probability":0.035714285714286,"occurences":28},"have":{"probability":0.21428571428571,"occurences":168},"are":{"probability":0.035714285714286,"occurences":28},"has":{"probability":0.035714285714286,"occurences":28},"shot":{"probability":0.071428571428571,"occurences":56},"constantly":{"probability":0.035714285714286,"occurences":28},"you":{"probability":0.035714285714286,"occurences":28},"do":{"probability":0.035714285714286,"occurences":28},"would":{"probability":0.035714285714286,"occurences":28},"makes":{"probability":0.035714285714286,"occurences":28},"is":{"probability":0.14285714285714,"occurences":112},"lost":{"probability":0.035714285714286,"occurences":28},"don't":{"probability":0.035714285714286,"occurences":28},"weren't":{"probability":0.035714285714286,"occurences":28},"doesn't":{"probability":0.071428571428571,"occurences":56},"I'm":{"probability":0.035714285714286,"occurences":28},"think":{"probability":0.035714285714286,"occurences":28},"get":{"probability":0.035714285714286,"occurences":28}},"falling":{"from":{"probability":1,"occurences":1}},"potential.":{"You":{"probability":1,"occurences":1}},"yourself.":{"You":{"probability":1,"occurences":1}},"designed":{"to":{"probability":1,"occurences":4}},"plates":{"which":{"probability":1,"occurences":1}},"run":{"up":{"probability":1,"occurences":1}},"basically":{"the":{"probability":0.5,"occurences":2},"saying":{"probability":0.5,"occurences":2}},"leg.":{"The":{"probability":1,"occurences":1}},"attached":{"to":{"probability":1,"occurences":4}},"other.":{"We're":{"probability":1,"occurences":1}},"'tumblrs')":{"who":{"probability":1,"occurences":1}},"wears":{"is":{"probability":1,"occurences":1}},"super":{"appreciative":{"probability":1,"occurences":1}},"change":{"if":{"probability":0.5,"occurences":2},"your":{"probability":0.5,"occurences":2}},"shield.":{"He":{"probability":1,"occurences":1}},"something":{"good":{"probability":0.25,"occurences":4},"don't":{"probability":0.25,"occurences":4},"they":{"probability":0.25,"occurences":4},"or":{"probability":0.25,"occurences":4}},"grade":{"9":{"probability":1,"occurences":1}},"formation.":{"In":{"probability":1,"occurences":1}},"enemy's":{"formation.":{"probability":1,"occurences":1}},"charge":{"into":{"probability":1,"occurences":1}},"formation":{"about":{"probability":0.5,"occurences":1},"and":{"probability":0.5,"occurences":1}},"great.":{"I":{"probability":1,"occurences":1}},"up":{"of":{"probability":0.57142857142857,"occurences":8},"that":{"probability":0.14285714285714,"occurences":2},"the":{"probability":0.14285714285714,"occurences":2},"in":{"probability":0.14285714285714,"occurences":2}},"metal":{"shield.":{"probability":0.5,"occurences":2},"plate":{"probability":0.5,"occurences":2}},"court":{"a":{"probability":0.5,"occurences":2},"meeting.":{"probability":0.5,"occurences":2}},"sword":{"and":{"probability":1,"occurences":1}},"ever":{"wanted":{"probability":0.25,"occurences":2},"talk":{"probability":0.25,"occurences":2},"asked":{"probability":0.25,"occurences":2},"seen.":{"probability":0.25,"occurences":2}},"a":{"loser.":{"probability":0.0058139534883721,"occurences":171},"$4,000":{"probability":0.0058139534883721,"occurences":171},"damn.":{"probability":0.0058139534883721,"occurences":171},"rarer":{"probability":0.0058139534883721,"occurences":171},"kitten.":{"probability":0.0058139534883721,"occurences":171},"metal":{"probability":0.0058139534883721,"occurences":171},"response":{"probability":0.0058139534883721,"occurences":171},"lot":{"probability":0.0058139534883721,"occurences":171},"divorce":{"probability":0.0058139534883721,"occurences":171},"couple":{"probability":0.017441860465116,"occurences":513},"huge":{"probability":0.011627906976744,"occurences":342},"gentleman.":{"probability":0.0058139534883721,"occurences":171},"term":{"probability":0.0058139534883721,"occurences":171},"short":{"probability":0.011627906976744,"occurences":342},"raise.":{"probability":0.0058139534883721,"occurences":171},"motion":{"probability":0.0058139534883721,"occurences":171},"Bible":{"probability":0.011627906976744,"occurences":342},"celebrity":{"probability":0.0058139534883721,"occurences":171},"single":{"probability":0.0058139534883721,"occurences":171},"little":{"probability":0.0058139534883721,"occurences":171},"knight's":{"probability":0.0058139534883721,"occurences":171},"difficult":{"probability":0.0058139534883721,"occurences":171},"background":{"probability":0.0058139534883721,"occurences":171},"person.":{"probability":0.011627906976744,"occurences":342},"friend":{"probability":0.0058139534883721,"occurences":171},"similar":{"probability":0.0058139534883721,"occurences":171},"shitty,":{"probability":0.0058139534883721,"occurences":171},"case":{"probability":0.0058139534883721,"occurences":171},"Muslim,":{"probability":0.011627906976744,"occurences":342},"caring":{"probability":0.0058139534883721,"occurences":171},"kid":{"probability":0.0058139534883721,"occurences":171},"big":{"probability":0.011627906976744,"occurences":342},"pastor.":{"probability":0.0058139534883721,"occurences":171},"massive":{"probability":0.0058139534883721,"occurences":171},"pony.":{"probability":0.0058139534883721,"occurences":171},"Jew,":{"probability":0.011627906976744,"occurences":342},"spoiled":{"probability":0.0058139534883721,"occurences":171},"wooden":{"probability":0.0058139534883721,"occurences":171},"girlfriend":{"probability":0.0058139534883721,"occurences":171},"troll,":{"probability":0.0058139534883721,"occurences":171},"new":{"probability":0.0058139534883721,"occurences":171},"Hindu,":{"probability":0.011627906976744,"occurences":342},"raise,":{"probability":0.011627906976744,"occurences":342},"liar,":{"probability":0.0058139534883721,"occurences":171},"settlement":{"probability":0.0058139534883721,"occurences":171},"better":{"probability":0.023255813953488,"occurences":684},"total":{"probability":0.0058139534883721,"occurences":171},"California":{"probability":0.0058139534883721,"occurences":171},"bodyguard":{"probability":0.0058139534883721,"occurences":171},"job":{"probability":0.0058139534883721,"occurences":171},"great":{"probability":0.023255813953488,"occurences":684},"joke":{"probability":0.0058139534883721,"occurences":171},"blog":{"probability":0.017441860465116,"occurences":513},"fucking":{"probability":0.011627906976744,"occurences":342},"collection":{"probability":0.011627906976744,"occurences":342},"steady":{"probability":0.0058139534883721,"occurences":171},"promotion.":{"probability":0.0058139534883721,"occurences":171},"job,":{"probability":0.0058139534883721,"occurences":171},"sermon":{"probability":0.011627906976744,"occurences":342},"person":{"probability":0.0058139534883721,"occurences":171},"horrible":{"probability":0.011627906976744,"occurences":342},"girl":{"probability":0.011627906976744,"occurences":342},"year.":{"probability":0.0058139534883721,"occurences":171},"bit":{"probability":0.0058139534883721,"occurences":171},"boy":{"probability":0.011627906976744,"occurences":342},"candy":{"probability":0.0058139534883721,"occurences":171},"weak":{"probability":0.0058139534883721,"occurences":171},"simple":{"probability":0.0058139534883721,"occurences":171},"promotion":{"probability":0.0058139534883721,"occurences":171},"crush":{"probability":0.011627906976744,"occurences":342},"kitten":{"probability":0.0058139534883721,"occurences":171},"group":{"probability":0.023255813953488,"occurences":684},"guy,":{"probability":0.0058139534883721,"occurences":171},"woman":{"probability":0.0058139534883721,"occurences":171},"guy":{"probability":0.0058139534883721,"occurences":171},"Buddhist,":{"probability":0.0058139534883721,"occurences":171},"head":{"probability":0.0058139534883721,"occurences":171},"selfish,":{"probability":0.011627906976744,"occurences":342},"mod":{"probability":0.0058139534883721,"occurences":171},"pony,":{"probability":0.011627906976744,"occurences":342},"Buddhist.":{"probability":0.0058139534883721,"occurences":171},"Buddhist":{"probability":0.023255813953488,"occurences":684},"copyright":{"probability":0.0058139534883721,"occurences":171},"bad":{"probability":0.023255813953488,"occurences":684},"married":{"probability":0.0058139534883721,"occurences":171},"king's":{"probability":0.0058139534883721,"occurences":171},"knight":{"probability":0.011627906976744,"occurences":342},"Christian":{"probability":0.0058139534883721,"occurences":171},"very":{"probability":0.023255813953488,"occurences":684},"Christian,":{"probability":0.011627906976744,"occurences":342},"girl.":{"probability":0.0058139534883721,"occurences":171},"boss":{"probability":0.0058139534883721,"occurences":171},"queen,":{"probability":0.0058139534883721,"occurences":171},"kid,":{"probability":0.0058139534883721,"occurences":171},"good":{"probability":0.12790697674419,"occurences":3762},"trip":{"probability":0.0058139534883721,"occurences":171},"'tumbl'":{"probability":0.0058139534883721,"occurences":171},"man":{"probability":0.0058139534883721,"occurences":171},"large":{"probability":0.0058139534883721,"occurences":171},"video":{"probability":0.0058139534883721,"occurences":171},"king":{"probability":0.052325581395349,"occurences":1539}},"girl.":{"She":{"probability":1,"occurences":1}},"friends,":{"I":{"probability":1,"occurences":1}},"asked":{"to":{"probability":0.5,"occurences":2},"for":{"probability":0.5,"occurences":2}},"community":{"is":{"probability":1,"occurences":4}},"up.":{"And":{"probability":1,"occurences":1}},"proof":{"of":{"probability":1,"occurences":1}},"When":{"a":{"probability":1,"occurences":1}},"dagger.":{"When":{"probability":1,"occurences":1}},"part":{"of":{"probability":0.33333333333333,"occurences":3},"is":{"probability":0.66666666666667,"occurences":6}},"walk":{"away,":{"probability":1,"occurences":1}},"hate":{"that":{"probability":1,"occurences":961}},"there.":{"You":{"probability":1,"occurences":4}},"generally":{"be":{"probability":1,"occurences":1}},"anymore,":{"I":{"probability":1,"occurences":1}},"word":{"\"motivation\".":{"probability":0.5,"occurences":1},"and":{"probability":0.5,"occurences":1}},"them,":{"the":{"probability":0.5,"occurences":2},"and":{"probability":0.5,"occurences":2}},"that's":{"not":{"probability":1,"occurences":1}},"order":{"and":{"probability":1,"occurences":1}},"carrying":{"a":{"probability":1,"occurences":1}},"girlfriend":{"and":{"probability":1,"occurences":1}},"explain":{"this":{"probability":1,"occurences":1}},"deal":{"with":{"probability":1,"occurences":4}},"troll,":{"and":{"probability":1,"occurences":1}},"claims":{"of":{"probability":1,"occurences":1}},"contacted":{"by":{"probability":1,"occurences":1}},"straightforward,":{"and":{"probability":1,"occurences":1}},"that?\"":{"But":{"probability":1,"occurences":1}},"away,":{"because":{"probability":1,"occurences":1}},"atheist.":{"I":{"probability":1,"occurences":1}},"saying":{"that":{"probability":1,"occurences":16}},"weak":{"fat":{"probability":1,"occurences":1}},"simple":{"cloak":{"probability":1,"occurences":1}},"shield":{"and":{"probability":1,"occurences":1}},"wearing":{"armor,":{"probability":1,"occurences":1}},"influential":{"species,":{"probability":1,"occurences":1}},"friends":{"anymore.":{"probability":0.16666666666667,"occurences":6},"for":{"probability":0.16666666666667,"occurences":6},"worth":{"probability":0.16666666666667,"occurences":6},"or":{"probability":0.16666666666667,"occurences":6},"again.":{"probability":0.33333333333333,"occurences":12}},"comparing":{"yourself":{"probability":0.5,"occurences":2},"myself":{"probability":0.5,"occurences":2}},"again.":{"But":{"probability":0.33333333333333,"occurences":3},"That's":{"probability":0.33333333333333,"occurences":3},"We":{"probability":0.33333333333333,"occurences":3}},"years":{"ago.":{"probability":0.5,"occurences":2},"and":{"probability":0.5,"occurences":2}},"useless.":{"Now":{"probability":0.5,"occurences":2},"For":{"probability":0.5,"occurences":2}},"related":{"content.":{"probability":1,"occurences":4}},"into":{"the":{"probability":0.2,"occurences":5},"battle,":{"probability":0.2,"occurences":5},"mail.":{"probability":0.4,"occurences":10},"this":{"probability":0.2,"occurences":5}},"necessarily":{"mean":{"probability":1,"occurences":4}},"inside":{"is":{"probability":1,"occurences":1}},"there":{"are":{"probability":0.4,"occurences":10},"will":{"probability":0.4,"occurences":10},"and":{"probability":0.2,"occurences":5}},"stand":{"what":{"probability":0.5,"occurences":1},"it.":{"probability":0.5,"occurences":1}},"play":{"video":{"probability":1,"occurences":1}},"opportunity,":{"but":{"probability":1,"occurences":4}},"we":{"were":{"probability":0.28571428571429,"occurences":14},"should":{"probability":0.14285714285714,"occurences":7},"became":{"probability":0.14285714285714,"occurences":7},"are":{"probability":0.14285714285714,"occurences":7},"knew":{"probability":0.14285714285714,"occurences":7},"first":{"probability":0.14285714285714,"occurences":7}},"actually":{"fighting":{"probability":0.5,"occurences":2},"have":{"probability":0.5,"occurences":2}},"enemy":{"the":{"probability":0.5,"occurences":2},"formation":{"probability":0.5,"occurences":2}},"based":{"on":{"probability":1,"occurences":1}},"site,":{"you'll":{"probability":1,"occurences":1}},"probably":{"be":{"probability":0.5,"occurences":2},"choose":{"probability":0.5,"occurences":2}},"me":{"visit":{"probability":0.032258064516129,"occurences":14},"that":{"probability":0.032258064516129,"occurences":14},"about":{"probability":0.032258064516129,"occurences":14},"best":{"probability":0.032258064516129,"occurences":14},"thing?":{"probability":0.032258064516129,"occurences":14},"so":{"probability":0.032258064516129,"occurences":14},"back":{"probability":0.032258064516129,"occurences":14},"subreddits":{"probability":0.064516129032258,"occurences":28},"rules.":{"probability":0.032258064516129,"occurences":14},"was":{"probability":0.032258064516129,"occurences":14},"to":{"probability":0.29032258064516,"occurences":126},"a":{"probability":0.16129032258065,"occurences":70},"that.":{"probability":0.032258064516129,"occurences":14},"she":{"probability":0.032258064516129,"occurences":14},"and":{"probability":0.032258064516129,"occurences":14},"very":{"probability":0.032258064516129,"occurences":14},"thing":{"probability":0.064516129032258,"occurences":28}},"interesting.":{"The":{"probability":1,"occurences":1}},"graduated":{"and":{"probability":1,"occurences":1}},"times.":{"The":{"probability":1,"occurences":1}},"cried":{"the":{"probability":0.25,"occurences":4},"and":{"probability":0.75,"occurences":12}},"3":{"jobs":{"probability":1,"occurences":1}},"'tumbl'":{"blog":{"probability":0.25,"occurences":4},"character":{"probability":0.25,"occurences":4},"(or":{"probability":0.5,"occurences":8}},"Hindu,":{"a":{"probability":0.5,"occurences":2},"I":{"probability":0.5,"occurences":2}},"there's":{"no":{"probability":0.5,"occurences":2},"been":{"probability":0.5,"occurences":2}},"dated":{"a":{"probability":1,"occurences":1}},"thing":{"I":{"probability":0.11111111111111,"occurences":2},"when":{"probability":0.11111111111111,"occurences":2},"don't":{"probability":0.11111111111111,"occurences":2},"or":{"probability":0.22222222222222,"occurences":4},"good":{"probability":0.11111111111111,"occurences":2},"for":{"probability":0.11111111111111,"occurences":2},"they":{"probability":0.11111111111111,"occurences":2},"and":{"probability":0.11111111111111,"occurences":2}},"equivalent":{"of":{"probability":1,"occurences":1}},"breed.":{"I":{"probability":1,"occurences":1}},"\"motivation\".":{"It's":{"probability":1,"occurences":1}},"person":{"I":{"probability":0.2,"occurences":5},"who":{"probability":0.6,"occurences":15},"or":{"probability":0.2,"occurences":5}},"king's":{"armor":{"probability":1,"occurences":1}},"devil,":{"and":{"probability":1,"occurences":1}},"Jewish":{"or":{"probability":1,"occurences":4}},"first":{"but":{"probability":0.25,"occurences":4},"met.":{"probability":0.25,"occurences":4},"filed":{"probability":0.25,"occurences":4},"part":{"probability":0.25,"occurences":4}},"detailed":{"description":{"probability":1,"occurences":1}},"second":{"part":{"probability":1,"occurences":1}},"things.":{"You":{"probability":1,"occurences":1}},"am":{"just":{"probability":0.33333333333333,"occurences":2},"doing":{"probability":0.33333333333333,"occurences":2},"to":{"probability":0.33333333333333,"occurences":2}},"friend's":{"place":{"probability":1,"occurences":1}},"company,":{"and":{"probability":1,"occurences":1}},"quote":{"is":{"probability":0.5,"occurences":2},"above,":{"probability":0.5,"occurences":2}},"subreddits":{"as":{"probability":1,"occurences":4}},"huge":{"step":{"probability":0.5,"occurences":2},"drama":{"probability":0.5,"occurences":2}},"alleges":{"that":{"probability":1,"occurences":1}},"home,":{"and":{"probability":1,"occurences":1}},"spear":{"or":{"probability":1,"occurences":1}},"about.":{"The":{"probability":1,"occurences":1}},"had":{"planned":{"probability":0.2,"occurences":5},"a":{"probability":0.4,"occurences":10},"issues":{"probability":0.4,"occurences":10}},"exactly":{"what":{"probability":1,"occurences":1}},"I'll":{"look":{"probability":0.33333333333333,"occurences":3},"lose":{"probability":0.33333333333333,"occurences":3},"get":{"probability":0.33333333333333,"occurences":3}},"medieval":{"military":{"probability":0.5,"occurences":2},"times.":{"probability":0.5,"occurences":2}},"committed":{"to":{"probability":1,"occurences":1}},"motion":{"was":{"probability":0.5,"occurences":1},"when":{"probability":0.5,"occurences":1}},"husband.":{"I":{"probability":1,"occurences":1}},"good":{"parent.":{"probability":0.2,"occurences":125},"coworker,":{"probability":0.04,"occurences":25},"person,":{"probability":0.08,"occurences":50},"person":{"probability":0.04,"occurences":25},"person.":{"probability":0.44,"occurences":275},"attitude,":{"probability":0.04,"occurences":25},"parent,":{"probability":0.04,"occurences":25},"things,":{"probability":0.04,"occurences":25},"for":{"probability":0.08,"occurences":50}},"haven't":{"been":{"probability":1,"occurences":1}},"view":{"by":{"probability":1,"occurences":1}},"created":{"by":{"probability":1,"occurences":1}},"plaintiff.":{"The":{"probability":1,"occurences":1}},"automatically":{"a":{"probability":0.33333333333333,"occurences":12},"going":{"probability":0.66666666666667,"occurences":24}},"phone":{"and":{"probability":1,"occurences":1}},"English":{"historian":{"probability":1,"occurences":1}},"gold,":{"let":{"probability":1,"occurences":1}},"reply":{"to":{"probability":0.5,"occurences":2},"with":{"probability":0.5,"occurences":2}},"entirely":{"out":{"probability":1,"occurences":1}},"think":{"that":{"probability":0.54166666666667,"occurences":312},"she":{"probability":0.041666666666667,"occurences":24},"I'm":{"probability":0.041666666666667,"occurences":24},"the":{"probability":0.083333333333333,"occurences":48},"it's":{"probability":0.083333333333333,"occurences":48},"there":{"probability":0.041666666666667,"occurences":24},"that.":{"probability":0.041666666666667,"occurences":24},"they":{"probability":0.041666666666667,"occurences":24},"we":{"probability":0.083333333333333,"occurences":48}},"shines":{"like":{"probability":1,"occurences":4}},"horseback,":{"let":{"probability":1,"occurences":4}},"collection":{"of":{"probability":1,"occurences":4}},"sued":{"for":{"probability":1,"occurences":1}},"world":{"is":{"probability":0.5,"occurences":2},"burn.":{"probability":0.5,"occurences":2}},"mail.":{"But":{"probability":0.6,"occurences":15},"And":{"probability":0.2,"occurences":5},"I'm":{"probability":0.2,"occurences":5}},"Muslim,":{"a":{"probability":0.5,"occurences":2},"I":{"probability":0.5,"occurences":2}},"mail,":{"and":{"probability":1,"occurences":4}},"something.":{"If":{"probability":1,"occurences":1}},"pays":{"the":{"probability":1,"occurences":1}},"difficult":{"post":{"probability":1,"occurences":1}},"field,":{"let":{"probability":1,"occurences":1}},"is":{"stronger":{"probability":0.010204081632653,"occurences":62},"useless.":{"probability":0.020408163265306,"occurences":124},"believed":{"probability":0.010204081632653,"occurences":62},"the":{"probability":0.040816326530612,"occurences":248},"not":{"probability":0.010204081632653,"occurences":62},"doing":{"probability":0.010204081632653,"occurences":62},"because":{"probability":0.010204081632653,"occurences":62},"job":{"probability":0.020408163265306,"occurences":124},"deliberately":{"probability":0.010204081632653,"occurences":62},"despite":{"probability":0.010204081632653,"occurences":62},"attached":{"probability":0.010204081632653,"occurences":62},"pretty":{"probability":0.010204081632653,"occurences":62},"my":{"probability":0.020408163265306,"occurences":124},"mind":{"probability":0.010204081632653,"occurences":62},"all":{"probability":0.010204081632653,"occurences":62},"girl":{"probability":0.010204081632653,"occurences":62},"that?\"":{"probability":0.010204081632653,"occurences":62},"post":{"probability":0.010204081632653,"occurences":62},"and":{"probability":0.010204081632653,"occurences":62},"country;":{"probability":0.010204081632653,"occurences":62},"actually":{"probability":0.010204081632653,"occurences":62},"mail,":{"probability":0.010204081632653,"occurences":62},"our":{"probability":0.020408163265306,"occurences":124},"suing":{"probability":0.020408163265306,"occurences":124},"that":{"probability":0.030612244897959,"occurences":186},"mail.":{"probability":0.030612244897959,"occurences":186},"an":{"probability":0.020408163265306,"occurences":124},"to":{"probability":0.020408163265306,"occurences":124},"just":{"probability":0.020408163265306,"occurences":124},"entirely":{"probability":0.010204081632653,"occurences":62},"married":{"probability":0.010204081632653,"occurences":62},"no":{"probability":0.010204081632653,"occurences":62},"past":{"probability":0.010204081632653,"occurences":62},"made":{"probability":0.010204081632653,"occurences":62},"sub":{"probability":0.010204081632653,"occurences":62},"battles.":{"probability":0.010204081632653,"occurences":62},"automatically":{"probability":0.010204081632653,"occurences":62},"where":{"probability":0.010204081632653,"occurences":62},"bodyguard":{"probability":0.020408163265306,"occurences":124},"calling":{"probability":0.010204081632653,"occurences":62},"for":{"probability":0.010204081632653,"occurences":62},"raise":{"probability":0.010204081632653,"occurences":62},"mail":{"probability":0.010204081632653,"occurences":62},"or":{"probability":0.020408163265306,"occurences":124},"armor":{"probability":0.12244897959184,"occurences":744},"is":{"probability":0.051020408163265,"occurences":310},"saying":{"probability":0.020408163265306,"occurences":124},"but":{"probability":0.010204081632653,"occurences":62},"involved":{"probability":0.010204081632653,"occurences":62},"what's":{"probability":0.010204081632653,"occurences":62},"out":{"probability":0.020408163265306,"occurences":124},"hard":{"probability":0.010204081632653,"occurences":62},"company,":{"probability":0.010204081632653,"occurences":62},"a":{"probability":0.10204081632653,"occurences":620},"alone.":{"probability":0.010204081632653,"occurences":62}},"battle.":{"Yet":{"probability":0.5,"occurences":2},"His":{"probability":0.5,"occurences":2}},"all;":{"he":{"probability":1,"occurences":1}},"fight":{"them":{"probability":0.25,"occurences":4},"his":{"probability":0.25,"occurences":4},"with":{"probability":0.25,"occurences":4},"in":{"probability":0.25,"occurences":4}},"make":{"you":{"probability":0.18181818181818,"occurences":22},"his":{"probability":0.18181818181818,"occurences":22},"money,":{"probability":0.090909090909091,"occurences":11},"money.":{"probability":0.090909090909091,"occurences":11},"because":{"probability":0.090909090909091,"occurences":11},"sure":{"probability":0.090909090909091,"occurences":11},"a":{"probability":0.090909090909091,"occurences":11},"my":{"probability":0.090909090909091,"occurences":11},"her":{"probability":0.090909090909091,"occurences":11}},"called":{"'tumblirls',":{"probability":1,"occurences":1}},"view.":{"I":{"probability":1,"occurences":1}},"'tumblirls',":{"which,":{"probability":1,"occurences":1}},"arms,":{"and":{"probability":1,"occurences":1}},"question":{"were":{"probability":0.33333333333333,"occurences":3},"is":{"probability":0.33333333333333,"occurences":3},"have":{"probability":0.33333333333333,"occurences":3}},"federal":{"court":{"probability":1,"occurences":1}},"let":{"him":{"probability":1,"occurences":49}},"country;":{"let":{"probability":1,"occurences":1}},"stop.":{"You":{"probability":1,"occurences":1}},"must":{"defend":{"probability":0.14285714285714,"occurences":7},"fight":{"probability":0.14285714285714,"occurences":7},"not":{"probability":0.71428571428571,"occurences":35}},"she":{"say":{"probability":0.076923076923077,"occurences":13},"realizes":{"probability":0.076923076923077,"occurences":13},"was":{"probability":0.23076923076923,"occurences":39},"had":{"probability":0.076923076923077,"occurences":13},"says,":{"probability":0.076923076923077,"occurences":13},"did":{"probability":0.15384615384615,"occurences":26},"told":{"probability":0.076923076923077,"occurences":13},"wanted":{"probability":0.076923076923077,"occurences":13},"just":{"probability":0.076923076923077,"occurences":13},"works":{"probability":0.076923076923077,"occurences":13}},"tells":{"us,":{"probability":0.5,"occurences":2},"me":{"probability":0.5,"occurences":2}},"you're":{"the":{"probability":0.125,"occurences":8},"not":{"probability":0.25,"occurences":16},"doing":{"probability":0.125,"occurences":8},"all":{"probability":0.125,"occurences":8},"looking":{"probability":0.25,"occurences":16},"going":{"probability":0.125,"occurences":8}},"just":{"to":{"probability":0.05,"occurences":20},"people":{"probability":0.05,"occurences":20},"trying":{"probability":0.05,"occurences":20},"be":{"probability":0.05,"occurences":20},"going":{"probability":0.05,"occurences":20},"want":{"probability":0.05,"occurences":20},"so":{"probability":0.15,"occurences":60},"hard":{"probability":0.05,"occurences":20},"how":{"probability":0.05,"occurences":20},"hands":{"probability":0.05,"occurences":20},"because":{"probability":0.05,"occurences":20},"the":{"probability":0.05,"occurences":20},"her":{"probability":0.05,"occurences":20},"a":{"probability":0.15,"occurences":60},"got":{"probability":0.1,"occurences":40}},"long":{"and":{"probability":0.66666666666667,"occurences":6},"as":{"probability":0.33333333333333,"occurences":3}},"bad":{"person.":{"probability":0.8,"occurences":20},"things":{"probability":0.2,"occurences":5}},"men":{"I":{"probability":0.125,"occurences":2},"who":{"probability":0.5,"occurences":8},"other":{"probability":0.125,"occurences":2},"you're":{"probability":0.125,"occurences":2},"tend":{"probability":0.125,"occurences":2}},"Islam":{"to":{"probability":1,"occurences":1}},"group":{"of":{"probability":0.8,"occurences":20},"that":{"probability":0.2,"occurences":5}},"more":{"facts,":{"probability":0.2,"occurences":5},"to":{"probability":0.2,"occurences":5},"mature":{"probability":0.2,"occurences":5},"information":{"probability":0.2,"occurences":5},"solid.":{"probability":0.2,"occurences":5}},"by":{"the":{"probability":0.14285714285714,"occurences":7},"default,":{"probability":0.14285714285714,"occurences":7},"showing":{"probability":0.14285714285714,"occurences":7},"Tumblr":{"probability":0.14285714285714,"occurences":7},"Tumblr.":{"probability":0.14285714285714,"occurences":7},"a":{"probability":0.14285714285714,"occurences":7},"this":{"probability":0.14285714285714,"occurences":7}},"pessimistic":{"person.":{"probability":1,"occurences":1}},"despite":{"being":{"probability":1,"occurences":1}},"better,":{"you":{"probability":1,"occurences":1}},"Jesus":{"or":{"probability":1,"occurences":1}},"hang":{"outs":{"probability":1,"occurences":1}},"sole":{"purpose":{"probability":1,"occurences":4}},"exercise.":{"It's":{"probability":1,"occurences":1}},"topic,":{"just":{"probability":1,"occurences":1}},"advice,":{"I":{"probability":1,"occurences":1}},"doesn't":{"believe":{"probability":0.5,"occurences":8},"necessarily":{"probability":0.5,"occurences":8}},"day":{"my":{"probability":0.33333333333333,"occurences":1},"and":{"probability":0.33333333333333,"occurences":1},"there.":{"probability":0.33333333333333,"occurences":1}},"someone":{"who":{"probability":1,"occurences":1}},"helping":{"others":{"probability":0.5,"occurences":2},"them":{"probability":0.5,"occurences":2}},"God":{"you":{"probability":0.28,"occurences":175},"when":{"probability":0.04,"occurences":25},"and":{"probability":0.04,"occurences":25},"is":{"probability":0.08,"occurences":50},"to":{"probability":0.56,"occurences":350}},"boss":{"came":{"probability":0.33333333333333,"occurences":3},"and":{"probability":0.33333333333333,"occurences":3},"who":{"probability":0.33333333333333,"occurences":3}},"is.":{"I":{"probability":0.66666666666667,"occurences":4},"Because":{"probability":0.16666666666667,"occurences":1},"So":{"probability":0.16666666666667,"occurences":1}},"follow":{"the":{"probability":1,"occurences":1}},"our":{"worth.":{"probability":0.076923076923077,"occurences":5},"work":{"probability":0.076923076923077,"occurences":5},"potential.":{"probability":0.076923076923077,"occurences":5},"relationship":{"probability":0.076923076923077,"occurences":5},"past.":{"probability":0.076923076923077,"occurences":5},"job.":{"probability":0.076923076923077,"occurences":5},"view.":{"probability":0.076923076923077,"occurences":5},"view":{"probability":0.076923076923077,"occurences":5},"life":{"probability":0.076923076923077,"occurences":5},"marriage":{"probability":0.076923076923077,"occurences":5},"will":{"probability":0.15384615384615,"occurences":10},"heart":{"probability":0.076923076923077,"occurences":5}},"Muslim":{"just":{"probability":0.2,"occurences":5},"to":{"probability":0.4,"occurences":10},"or":{"probability":0.4,"occurences":10}},"kitten":{"and":{"probability":1,"occurences":1}},"Jew":{"or":{"probability":1,"occurences":1}},"without":{"armor":{"probability":0.16666666666667,"occurences":6},"his":{"probability":0.5,"occurences":18},"some":{"probability":0.16666666666667,"occurences":6},"hurting":{"probability":0.16666666666667,"occurences":6}},"woman":{"with":{"probability":1,"occurences":1}},"rules.":{"I":{"probability":1,"occurences":1}},"care":{"what":{"probability":0.33333333333333,"occurences":3},"of":{"probability":0.66666666666667,"occurences":6}},"consequences":{"of":{"probability":1,"occurences":1}},"loved":{"to":{"probability":1,"occurences":4}},"past":{"week":{"probability":1,"occurences":1}},"needs":{"to":{"probability":1,"occurences":9}},"passage":{"or":{"probability":1,"occurences":1}},"everyone":{"I":{"probability":0.14285714285714,"occurences":7},"who":{"probability":0.14285714285714,"occurences":7},"she":{"probability":0.14285714285714,"occurences":7},"else":{"probability":0.14285714285714,"occurences":7},"needs":{"probability":0.42857142857143,"occurences":21}},"Buddhist.":{"So":{"probability":1,"occurences":1}},"time.":{"I":{"probability":1,"occurences":1}},"things":{"you":{"probability":0.16666666666667,"occurences":6},"that":{"probability":0.33333333333333,"occurences":12},"to":{"probability":0.16666666666667,"occurences":6},"too.":{"probability":0.16666666666667,"occurences":6},"all":{"probability":0.16666666666667,"occurences":6}},"these":{"things":{"probability":1,"occurences":1}},"pray":{"to":{"probability":0.5,"occurences":2},"or":{"probability":0.5,"occurences":2}},"understand":{"what":{"probability":1,"occurences":1}},"fan":{"of":{"probability":0.25,"occurences":4},"is":{"probability":0.5,"occurences":8},"in":{"probability":0.25,"occurences":4}},"Buddhist":{"and":{"probability":0.25,"occurences":4},"to":{"probability":0.25,"occurences":4},"or":{"probability":0.5,"occurences":8}},"doing.":{"I":{"probability":0.5,"occurences":2},"I'm":{"probability":0.5,"occurences":2}},"Buddhist,":{"a":{"probability":1,"occurences":1}},"Jew,":{"a":{"probability":0.5,"occurences":2},"I":{"probability":0.5,"occurences":2}},"find":{"the":{"probability":0.16666666666667,"occurences":6},"a":{"probability":0.5,"occurences":18},"who":{"probability":0.16666666666667,"occurences":6},"out":{"probability":0.16666666666667,"occurences":6}},"front":{"of":{"probability":1,"occurences":4}},"friend.":{"I":{"probability":1,"occurences":1}},"expert":{"on":{"probability":1,"occurences":1}},"pastor.":{"I":{"probability":1,"occurences":1}},"sermon":{"but":{"probability":0.5,"occurences":2},"by":{"probability":0.5,"occurences":2}},"video":{"of":{"probability":0.5,"occurences":2},"games":{"probability":0.5,"occurences":2}},"think,":{"though,":{"probability":1,"occurences":1}},"sucks,":{"but":{"probability":1,"occurences":1}},"their":{"religion":{"probability":0.14285714285714,"occurences":7},"head":{"probability":0.14285714285714,"occurences":7},"husband":{"probability":0.14285714285714,"occurences":7},"husband?":{"probability":0.14285714285714,"occurences":7},"bills.":{"probability":0.14285714285714,"occurences":7},"money,":{"probability":0.14285714285714,"occurences":7},"'harassment'.":{"probability":0.14285714285714,"occurences":7}},"went":{"into":{"probability":0.25,"occurences":4},"on":{"probability":0.25,"occurences":4},"to":{"probability":0.5,"occurences":8}},"battle,":{"he":{"probability":1,"occurences":1}},"week":{"I":{"probability":1,"occurences":1}},"off,":{"and":{"probability":1,"occurences":1}},"answers.":{"I'm":{"probability":1,"occurences":1}},"Why":{"you":{"probability":0.5,"occurences":2},"would":{"probability":0.5,"occurences":2}},"However,":{"it":{"probability":1,"occurences":1}},"not":{"about":{"probability":0.041666666666667,"occurences":24},"\"oh,":{"probability":0.041666666666667,"occurences":24},"entitled.":{"probability":0.041666666666667,"occurences":24},"fight":{"probability":0.041666666666667,"occurences":24},"dating":{"probability":0.125,"occurences":72},"going":{"probability":0.083333333333333,"occurences":48},"all;":{"probability":0.041666666666667,"occurences":24},"an":{"probability":0.083333333333333,"occurences":48},"the":{"probability":0.041666666666667,"occurences":24},"quite":{"probability":0.041666666666667,"occurences":24},"convinced":{"probability":0.041666666666667,"occurences":24},"go":{"probability":0.20833333333333,"occurences":120},"a":{"probability":0.125,"occurences":72},"in":{"probability":0.041666666666667,"occurences":24}},"source":{"of":{"probability":1,"occurences":1}},"lot":{"of":{"probability":0.5,"occurences":2},"better,":{"probability":0.5,"occurences":2}},"\"harassment":{"and":{"probability":1,"occurences":1}},"met.":{"I":{"probability":1,"occurences":1}},"degree,":{"you":{"probability":1,"occurences":1}},"couple":{"of":{"probability":1,"occurences":9}},"you":{"visit":{"probability":0.013333333333333,"occurences":75},"happy,":{"probability":0.013333333333333,"occurences":75},"and":{"probability":0.013333333333333,"occurences":75},"have":{"probability":0.053333333333333,"occurences":300},"are":{"probability":0.2,"occurences":1125},"joy.":{"probability":0.013333333333333,"occurences":75},"need":{"probability":0.25333333333333,"occurences":1425},"the":{"probability":0.013333333333333,"occurences":75},"can":{"probability":0.066666666666667,"occurences":375},"happy.":{"probability":0.013333333333333,"occurences":75},"taking":{"probability":0.013333333333333,"occurences":75},"wear":{"probability":0.013333333333333,"occurences":75},"for":{"probability":0.013333333333333,"occurences":75},"don't":{"probability":0.10666666666667,"occurences":600},"guys":{"probability":0.013333333333333,"occurences":75},"want":{"probability":0.04,"occurences":225},"love":{"probability":0.04,"occurences":225},"wouldn't":{"probability":0.013333333333333,"occurences":75},"love.":{"probability":0.013333333333333,"occurences":75},"like,":{"probability":0.013333333333333,"occurences":75},"that":{"probability":0.013333333333333,"occurences":75},"like":{"probability":0.026666666666667,"occurences":150},"will":{"probability":0.013333333333333,"occurences":75},"should":{"probability":0.013333333333333,"occurences":75}},"each":{"other.":{"probability":0.25,"occurences":4},"other":{"probability":0.75,"occurences":12}},"response":{"to":{"probability":1,"occurences":1}},"fan.":{"I":{"probability":1,"occurences":1}},"saved.":{"I":{"probability":0.33333333333333,"occurences":3},"And":{"probability":0.33333333333333,"occurences":3},"But":{"probability":0.33333333333333,"occurences":3}},"skeptical":{"at":{"probability":1,"occurences":1}},"thought":{"I":{"probability":0.5,"occurences":2},"she":{"probability":0.5,"occurences":2}},"know":{"that":{"probability":0.045454545454545,"occurences":22},"how":{"probability":0.090909090909091,"occurences":44},"where":{"probability":0.090909090909091,"occurences":44},"exactly":{"probability":0.045454545454545,"occurences":22},"what":{"probability":0.5,"occurences":242},"your":{"probability":0.13636363636364,"occurences":66},"a":{"probability":0.045454545454545,"occurences":22},"who":{"probability":0.045454545454545,"occurences":22}},"fan,":{"and":{"probability":1,"occurences":1}},"reason":{"I":{"probability":0.33333333333333,"occurences":3},"to":{"probability":0.33333333333333,"occurences":3},"she":{"probability":0.33333333333333,"occurences":3}},"facts,":{"and":{"probability":1,"occurences":1}},"discussion":{"of":{"probability":1,"occurences":1}},"Tumblr.":{"I":{"probability":1,"occurences":1}},"historian":{"who":{"probability":1,"occurences":1}},"thinks":{"it's":{"probability":1,"occurences":1}},"convinced":{"that":{"probability":1,"occurences":1}},"damages":{"of":{"probability":1,"occurences":1}},"ages'":{"Tumblr":{"probability":1,"occurences":1}},"come":{"visit":{"probability":0.33333333333333,"occurences":2},"a":{"probability":0.33333333333333,"occurences":2},"back":{"probability":0.33333333333333,"occurences":2}},"actual":{"person.":{"probability":1,"occurences":1}},"pointing":{"out":{"probability":1,"occurences":1}},"background":{"in":{"probability":1,"occurences":1}},"enemy.":{"I":{"probability":1,"occurences":1}},"posted":{"a":{"probability":1,"occurences":1}},"shot":{"a":{"probability":1,"occurences":4}},"Not":{"to":{"probability":1,"occurences":1}},"truly":{"think":{"probability":1,"occurences":1}},"20,":{"even":{"probability":1,"occurences":1}},"shitty,":{"greedy":{"probability":1,"occurences":1}},"impossible":{"to":{"probability":1,"occurences":1}},"liar,":{"a":{"probability":1,"occurences":1}},"showing":{"how":{"probability":1,"occurences":1}},"You":{"can":{"probability":0.08,"occurences":50},"have":{"probability":0.04,"occurences":25},"will":{"probability":0.04,"occurences":25},"are":{"probability":0.28,"occurences":175},"put":{"probability":0.04,"occurences":25},"need":{"probability":0.52,"occurences":325}},"luck.":{"I":{"probability":1,"occurences":1}},"against":{"the":{"probability":1,"occurences":1}},"or":{"their":{"probability":0.011627906976744,"occurences":32},"where":{"probability":0.011627906976744,"occurences":32},"anybody":{"probability":0.011627906976744,"occurences":32},"Jewish":{"probability":0.023255813953488,"occurences":64},"the":{"probability":0.069767441860465,"occurences":192},"then":{"probability":0.011627906976744,"occurences":32},"me.":{"probability":0.011627906976744,"occurences":32},"them":{"probability":0.034883720930233,"occurences":96},"taking":{"probability":0.011627906976744,"occurences":32},"them,":{"probability":0.011627906976744,"occurences":32},"you.":{"probability":0.023255813953488,"occurences":64},"advice,":{"probability":0.011627906976744,"occurences":32},"Islam":{"probability":0.011627906976744,"occurences":32},"shines":{"probability":0.011627906976744,"occurences":32},"her.":{"probability":0.011627906976744,"occurences":32},"dagger.":{"probability":0.011627906976744,"occurences":32},"her":{"probability":0.023255813953488,"occurences":64},"damages":{"probability":0.011627906976744,"occurences":32},"those":{"probability":0.011627906976744,"occurences":32},"anything.":{"probability":0.011627906976744,"occurences":32},"that":{"probability":0.069767441860465,"occurences":192},"Muslim.":{"probability":0.011627906976744,"occurences":32},"$15":{"probability":0.011627906976744,"occurences":32},"be":{"probability":0.011627906976744,"occurences":32},"are":{"probability":0.011627906976744,"occurences":32},"'tumblr')":{"probability":0.023255813953488,"occurences":64},"so":{"probability":0.011627906976744,"occurences":32},"into":{"probability":0.023255813953488,"occurences":64},"good":{"probability":0.011627906976744,"occurences":32},"he":{"probability":0.011627906976744,"occurences":32},"two":{"probability":0.011627906976744,"occurences":32},"listen":{"probability":0.023255813953488,"occurences":64},"'tumblrs')":{"probability":0.011627906976744,"occurences":32},"they":{"probability":0.023255813953488,"occurences":64},"which":{"probability":0.011627906976744,"occurences":32},"something.":{"probability":0.011627906976744,"occurences":32},"gloves,":{"probability":0.011627906976744,"occurences":32},"something":{"probability":0.011627906976744,"occurences":32},"would":{"probability":0.023255813953488,"occurences":64},"Jew":{"probability":0.011627906976744,"occurences":32},"me":{"probability":0.011627906976744,"occurences":32},"is":{"probability":0.011627906976744,"occurences":32},"Muslim":{"probability":0.058139534883721,"occurences":160},"but":{"probability":0.011627906976744,"occurences":32},"based":{"probability":0.011627906976744,"occurences":32},"over":{"probability":0.011627906976744,"occurences":32},"divorced,":{"probability":0.011627906976744,"occurences":32},"2":{"probability":0.011627906976744,"occurences":32},"and":{"probability":0.046511627906977,"occurences":128},"a":{"probability":0.1046511627907,"occurences":288},"this.":{"probability":0.011627906976744,"occurences":32}},"gone":{"on":{"probability":1,"occurences":1}},"reached.":{"However,":{"probability":1,"occurences":1}},"him.\"":{"If":{"probability":1,"occurences":1}},"amateur":{"photographer":{"probability":1,"occurences":1}},"so?":{"My":{"probability":1,"occurences":1}},"plus":{"costs":{"probability":1,"occurences":1}},"it's":{"hard":{"probability":0.14285714285714,"occurences":6},"that":{"probability":0.14285714285714,"occurences":6},"not":{"probability":0.14285714285714,"occurences":6},"policies,":{"probability":0.14285714285714,"occurences":6},"a":{"probability":0.28571428571429,"occurences":12},"going":{"probability":0.14285714285714,"occurences":6}},"quite":{"sure":{"probability":1,"occurences":1}},"joy.":{"A":{"probability":1,"occurences":1}},"policies,":{"but":{"probability":1,"occurences":1}},"with":{"myself":{"probability":0.029411764705882,"occurences":34},"his":{"probability":0.17647058823529,"occurences":204},"people":{"probability":0.029411764705882,"occurences":34},"my":{"probability":0.058823529411765,"occurences":68},"friends":{"probability":0.029411764705882,"occurences":34},"myself.":{"probability":0.029411764705882,"occurences":34},"dating":{"probability":0.058823529411765,"occurences":68},"a":{"probability":0.20588235294118,"occurences":238},"the":{"probability":0.088235294117647,"occurences":102},"arms":{"probability":0.088235294117647,"occurences":102},"her":{"probability":0.029411764705882,"occurences":34},"tumblr":{"probability":0.058823529411765,"occurences":68},"more":{"probability":0.029411764705882,"occurences":34},"him":{"probability":0.029411764705882,"occurences":34},"difficult.":{"probability":0.029411764705882,"occurences":34},"those":{"probability":0.029411764705882,"occurences":34}},"alone.":{"If":{"probability":0.5,"occurences":2},"I":{"probability":0.5,"occurences":2}},"infringement.":{"The":{"probability":1,"occurences":1}},"weren't":{"able":{"probability":0.5,"occurences":2},"friends":{"probability":0.5,"occurences":2}},"videos":{"is":{"probability":1,"occurences":1}},"have":{"advice":{"probability":0.029411764705882,"occurences":34},"to":{"probability":0.17647058823529,"occurences":204},"known":{"probability":0.029411764705882,"occurences":34},"passions":{"probability":0.029411764705882,"occurences":34},"loved":{"probability":0.058823529411765,"occurences":68},"been":{"probability":0.11764705882353,"occurences":136},"said,":{"probability":0.029411764705882,"occurences":34},"time":{"probability":0.029411764705882,"occurences":34},"any":{"probability":0.058823529411765,"occurences":68},"had":{"probability":0.058823529411765,"occurences":68},"it":{"probability":0.029411764705882,"occurences":34},"changed":{"probability":0.029411764705882,"occurences":34},"gone":{"probability":0.029411764705882,"occurences":34},"no":{"probability":0.058823529411765,"occurences":68},"worked":{"probability":0.029411764705882,"occurences":34},"ever":{"probability":0.029411764705882,"occurences":34},"a":{"probability":0.11764705882353,"occurences":136},"the":{"probability":0.058823529411765,"occurences":68}},"believe":{"the":{"probability":0.03448275862069,"occurences":29},"they":{"probability":0.03448275862069,"occurences":29},"in":{"probability":0.93103448275862,"occurences":783}},"bills.":{"I":{"probability":1,"occurences":1}},"passions":{"in":{"probability":1,"occurences":1}},"use":{"she's":{"probability":0.071428571428571,"occurences":1},"I":{"probability":0.14285714285714,"occurences":2},"and":{"probability":0.071428571428571,"occurences":1},"they":{"probability":0.071428571428571,"occurences":1},"of":{"probability":0.14285714285714,"occurences":2},"everyone":{"probability":0.14285714285714,"occurences":2},"today":{"probability":0.071428571428571,"occurences":1},"I'm":{"probability":0.28571428571429,"occurences":4}},"able":{"to":{"probability":1,"occurences":16}},"There":{"is":{"probability":0.33333333333333,"occurences":3},"are":{"probability":0.66666666666667,"occurences":6}},"her.":{"We're":{"probability":0.2,"occurences":2},"I":{"probability":0.8,"occurences":8}},"photographer":{"and":{"probability":1,"occurences":1}},"causing":{"\"harassment":{"probability":1,"occurences":1}},"great":{"place":{"probability":0.25,"occurences":4},"father":{"probability":0.25,"occurences":4},"husband":{"probability":0.25,"occurences":4},"chance":{"probability":0.25,"occurences":4}},"wear":{"to":{"probability":0.5,"occurences":2},"without":{"probability":0.5,"occurences":2}},"this":{"raise":{"probability":0.071428571428571,"occurences":14},"and":{"probability":0.071428571428571,"occurences":14},"or":{"probability":0.071428571428571,"occurences":14},"is":{"probability":0.28571428571429,"occurences":56},"but":{"probability":0.071428571428571,"occurences":14},"post":{"probability":0.071428571428571,"occurences":14},"despite":{"probability":0.071428571428571,"occurences":14},"job":{"probability":0.071428571428571,"occurences":14},"company,":{"probability":0.071428571428571,"occurences":14},"girl":{"probability":0.071428571428571,"occurences":14},"alone.":{"probability":0.071428571428571,"occurences":14}},"provide":{"more":{"probability":0.5,"occurences":2},"support":{"probability":0.5,"occurences":2}},"said":{"that":{"probability":1,"occurences":1}},"filed":{"in":{"probability":0.5,"occurences":2},"to":{"probability":0.5,"occurences":2}},"re-filed.":{"The":{"probability":1,"occurences":1}},"was":{"hurting.":{"probability":0.03125,"occurences":32},"just":{"probability":0.09375,"occurences":96},"so":{"probability":0.03125,"occurences":32},"created":{"probability":0.03125,"occurences":32},"not":{"probability":0.03125,"occurences":32},"her":{"probability":0.03125,"occurences":32},"happy":{"probability":0.03125,"occurences":32},"always":{"probability":0.0625,"occurences":64},"20,":{"probability":0.03125,"occurences":32},"crying.":{"probability":0.03125,"occurences":32},"basically":{"probability":0.03125,"occurences":32},"a":{"probability":0.09375,"occurences":96},"filed":{"probability":0.03125,"occurences":32},"heartbroken.":{"probability":0.03125,"occurences":32},"dismissed":{"probability":0.03125,"occurences":32},"granted":{"probability":0.03125,"occurences":32},"asked":{"probability":0.03125,"occurences":32},"going":{"probability":0.03125,"occurences":32},"the":{"probability":0.0625,"occurences":64},"working":{"probability":0.03125,"occurences":32},"excited.":{"probability":0.03125,"occurences":32},"everything":{"probability":0.03125,"occurences":32},"designed":{"probability":0.03125,"occurences":32},"first":{"probability":0.03125,"occurences":32},"very":{"probability":0.03125,"occurences":32},"also":{"probability":0.03125,"occurences":32}},"fairly":{"sure":{"probability":1,"occurences":1}},"crazy,":{"to":{"probability":1,"occurences":1}},"reddit's":{"policies,":{"probability":1,"occurences":1}},"didn't":{"call":{"probability":0.14285714285714,"occurences":7},"do":{"probability":0.14285714285714,"occurences":7},"have":{"probability":0.14285714285714,"occurences":7},"want":{"probability":0.42857142857143,"occurences":21},"because":{"probability":0.14285714285714,"occurences":7}},"year.":{"I":{"probability":1,"occurences":1}},"any":{"advice":{"probability":0.33333333333333,"occurences":3},"hang":{"probability":0.33333333333333,"occurences":3},"friends":{"probability":0.33333333333333,"occurences":3}},"out":{"that":{"probability":0.037037037037037,"occurences":10},"where":{"probability":0.037037037037037,"occurences":10},"women":{"probability":0.037037037037037,"occurences":10},"hurting":{"probability":0.037037037037037,"occurences":10},"armor":{"probability":0.037037037037037,"occurences":10},"why":{"probability":0.037037037037037,"occurences":10},"the":{"probability":0.074074074074074,"occurences":20},"what":{"probability":0.074074074074074,"occurences":20},"taking":{"probability":0.037037037037037,"occurences":10},"this":{"probability":0.037037037037037,"occurences":10},"everything.":{"probability":0.037037037037037,"occurences":10},"of":{"probability":0.11111111111111,"occurences":30},"his":{"probability":0.11111111111111,"occurences":30},"it.":{"probability":0.037037037037037,"occurences":10},"getting":{"probability":0.037037037037037,"occurences":10},"it,":{"probability":0.037037037037037,"occurences":10},"some":{"probability":0.037037037037037,"occurences":10},"to":{"probability":0.14814814814815,"occurences":40}},"going":{"to":{"probability":1,"occurences":324}},"older":{"than":{"probability":0.5,"occurences":2},"or":{"probability":0.5,"occurences":2}},"some":{"very":{"probability":0.33333333333333,"occurences":3},"subreddits":{"probability":0.66666666666667,"occurences":6}},"feel":{"like":{"probability":0.6,"occurences":15},"a":{"probability":0.2,"occurences":5},"so":{"probability":0.2,"occurences":5}},"anything.":{"I":{"probability":1,"occurences":1}},"advice":{"on":{"probability":0.5,"occurences":2},"for":{"probability":0.5,"occurences":2}},"like.":{"Not":{"probability":1,"occurences":1}},"mistakes.":{"I":{"probability":1,"occurences":1}},"save":{"them":{"probability":0.2,"occurences":5},"a":{"probability":0.2,"occurences":5},"them.":{"probability":0.4,"occurences":10},"anybody.":{"probability":0.2,"occurences":5}},"that":{"someone":{"probability":0.0094339622641509,"occurences":106},"their":{"probability":0.0094339622641509,"occurences":106},"everyone":{"probability":0.028301886792453,"occurences":318},"his":{"probability":0.028301886792453,"occurences":318},"the":{"probability":0.066037735849057,"occurences":742},"she's":{"probability":0.018867924528302,"occurences":212},"if":{"probability":0.094339622641509,"occurences":1060},"job,":{"probability":0.0094339622641509,"occurences":106},"matters.":{"probability":0.0094339622641509,"occurences":106},"I'm":{"probability":0.028301886792453,"occurences":318},"they":{"probability":0.028301886792453,"occurences":318},"doesn't":{"probability":0.018867924528302,"occurences":212},"you":{"probability":0.23584905660377,"occurences":2650},"pays":{"probability":0.0094339622641509,"occurences":106},"people":{"probability":0.16981132075472,"occurences":1908},"bring":{"probability":0.0094339622641509,"occurences":106},"was":{"probability":0.0094339622641509,"occurences":106},"I'll":{"probability":0.018867924528302,"occurences":212},"particular":{"probability":0.0094339622641509,"occurences":106},"it":{"probability":0.018867924528302,"occurences":212},"I":{"probability":0.075471698113208,"occurences":848},"degree,":{"probability":0.0094339622641509,"occurences":106},"it's":{"probability":0.0094339622641509,"occurences":106},"is":{"probability":0.0094339622641509,"occurences":106},"he":{"probability":0.0094339622641509,"occurences":106},"there's":{"probability":0.0094339622641509,"occurences":106},"a":{"probability":0.028301886792453,"occurences":318},"our":{"probability":0.018867924528302,"occurences":212}},"armor":{"that":{"probability":0.10526315789474,"occurences":38},"and":{"probability":0.21052631578947,"occurences":76},"would":{"probability":0.10526315789474,"occurences":38},"is":{"probability":0.052631578947368,"occurences":19},"based":{"probability":0.052631578947368,"occurences":19},"but":{"probability":0.052631578947368,"occurences":19},"he":{"probability":0.052631578947368,"occurences":19},"shines":{"probability":0.052631578947368,"occurences":19},"into":{"probability":0.10526315789474,"occurences":38},"which":{"probability":0.052631578947368,"occurences":19},"gloves,":{"probability":0.052631578947368,"occurences":19},"they":{"probability":0.052631578947368,"occurences":19},"then":{"probability":0.052631578947368,"occurences":19}},"'all":{"ages'":{"probability":1,"occurences":1}},"religion":{"is":{"probability":0.5,"occurences":2},"and":{"probability":0.5,"occurences":2}},"been":{"much":{"probability":0.083333333333333,"occurences":12},"and":{"probability":0.083333333333333,"occurences":12},"through":{"probability":0.083333333333333,"occurences":12},"in":{"probability":0.083333333333333,"occurences":12},"on":{"probability":0.16666666666667,"occurences":24},"contacted":{"probability":0.083333333333333,"occurences":12},"working":{"probability":0.083333333333333,"occurences":12},"re-filed.":{"probability":0.083333333333333,"occurences":12},"so":{"probability":0.083333333333333,"occurences":12},"married":{"probability":0.16666666666667,"occurences":24}},"rarer":{"breed.":{"probability":1,"occurences":1}},"appreciate":{"you":{"probability":1,"occurences":1}},"then":{"run":{"probability":0.1,"occurences":10},"that":{"probability":0.2,"occurences":20},"he":{"probability":0.2,"occurences":20},"she":{"probability":0.1,"occurences":10},"later":{"probability":0.1,"occurences":10},"are":{"probability":0.1,"occurences":10},"they":{"probability":0.1,"occurences":10},"you":{"probability":0.1,"occurences":10}},"life.":{"You":{"probability":0.25,"occurences":4},"I":{"probability":0.5,"occurences":8},"This":{"probability":0.25,"occurences":4}},"told":{"you":{"probability":0.2,"occurences":5},"me":{"probability":0.6,"occurences":15},"my":{"probability":0.2,"occurences":5}},"eat.":{"You":{"probability":0.5,"occurences":1},"I":{"probability":0.5,"occurences":1}},"either":{"a":{"probability":0.5,"occurences":2},"older":{"probability":0.5,"occurences":2}},"for":{"damages":{"probability":0.032258064516129,"occurences":31},"their":{"probability":0.032258064516129,"occurences":31},"$15":{"probability":0.032258064516129,"occurences":31},"so":{"probability":0.032258064516129,"occurences":31},"the":{"probability":0.12903225806452,"occurences":124},"her":{"probability":0.064516129032258,"occurences":62},"me.":{"probability":0.032258064516129,"occurences":31},"them":{"probability":0.096774193548387,"occurences":93},"taking":{"probability":0.032258064516129,"occurences":31},"them,":{"probability":0.032258064516129,"occurences":31},"you.":{"probability":0.064516129032258,"occurences":62},"me":{"probability":0.032258064516129,"occurences":31},"advice,":{"probability":0.032258064516129,"occurences":31},"two":{"probability":0.032258064516129,"occurences":31},"this.":{"probability":0.032258064516129,"occurences":31},"over":{"probability":0.032258064516129,"occurences":31},"good":{"probability":0.032258064516129,"occurences":31},"2":{"probability":0.032258064516129,"occurences":31},"her.":{"probability":0.032258064516129,"occurences":31},"a":{"probability":0.12903225806452,"occurences":124},"those":{"probability":0.032258064516129,"occurences":31}},"yourself":{"to":{"probability":1,"occurences":1}},"Then":{"I":{"probability":1,"occurences":1}},"better":{"person.":{"probability":0.2,"occurences":5},"car":{"probability":0.2,"occurences":5},"mobile":{"probability":0.2,"occurences":5},"computer":{"probability":0.2,"occurences":5},"at":{"probability":0.2,"occurences":5}},"harassment.":{"The":{"probability":1,"occurences":1}},"become":{"a":{"probability":1,"occurences":1}},"16":{"and":{"probability":1,"occurences":1}},"place":{"was":{"probability":0.33333333333333,"occurences":3},"to":{"probability":0.33333333333333,"occurences":3},"and":{"probability":0.33333333333333,"occurences":3}},"Yet":{"if":{"probability":1,"occurences":1}},"'tumblrites'":{"on":{"probability":0.125,"occurences":8},"to":{"probability":0.125,"occurences":8},"have":{"probability":0.125,"occurences":8},"in":{"probability":0.25,"occurences":16},"are":{"probability":0.125,"occurences":8},"being":{"probability":0.125,"occurences":8},"(or":{"probability":0.125,"occurences":8}},"divorced,":{"or":{"probability":1,"occurences":1}},"go":{"and":{"probability":0.090909090909091,"occurences":44},"to":{"probability":0.13636363636364,"occurences":66},"through":{"probability":0.045454545454545,"occurences":22},"without":{"probability":0.18181818181818,"occurences":88},"armed":{"probability":0.45454545454545,"occurences":220},"with":{"probability":0.045454545454545,"occurences":22},"home,":{"probability":0.045454545454545,"occurences":22}},"time":{"to":{"probability":0.8,"occurences":20},"was":{"probability":0.2,"occurences":5}},"end":{"his":{"probability":0.14285714285714,"occurences":1},"who":{"probability":0.14285714285714,"occurences":1},"all":{"probability":0.14285714285714,"occurences":1},"of":{"probability":0.14285714285714,"occurences":1},"and":{"probability":0.14285714285714,"occurences":1},"to":{"probability":0.14285714285714,"occurences":1},"it":{"probability":0.14285714285714,"occurences":1}},"get":{"that":{"probability":0.071428571428571,"occurences":14},"fired":{"probability":0.071428571428571,"occurences":14},"divorced":{"probability":0.071428571428571,"occurences":14},"why":{"probability":0.071428571428571,"occurences":14},"the":{"probability":0.14285714285714,"occurences":28},"them,":{"probability":0.071428571428571,"occurences":14},"over":{"probability":0.071428571428571,"occurences":14},"sex.":{"probability":0.071428571428571,"occurences":14},"a":{"probability":0.071428571428571,"occurences":14},"my":{"probability":0.14285714285714,"occurences":28},"promoted.":{"probability":0.071428571428571,"occurences":14},"that.":{"probability":0.071428571428571,"occurences":14}},"ordinary.":{"I'm":{"probability":1,"occurences":1}},"bed":{"to":{"probability":1,"occurences":1}},"and":{"forums.":{"probability":0.017699115044248,"occurences":220},"where":{"probability":0.0088495575221239,"occurences":110},"she":{"probability":0.026548672566372,"occurences":330},"proof":{"probability":0.0088495575221239,"occurences":110},"we're":{"probability":0.0088495575221239,"occurences":110},"I've":{"probability":0.017699115044248,"occurences":220},"the":{"probability":0.044247787610619,"occurences":550},"then":{"probability":0.035398230088496,"occurences":440},"told":{"probability":0.0088495575221239,"occurences":110},"doxing":{"probability":0.0088495575221239,"occurences":110},"tells":{"probability":0.0088495575221239,"occurences":110},"posted":{"probability":0.0088495575221239,"occurences":110},"keep":{"probability":0.0088495575221239,"occurences":110},"blogger":{"probability":0.0088495575221239,"occurences":110},"makes":{"probability":0.0088495575221239,"occurences":110},"a":{"probability":0.035398230088496,"occurences":440},"and":{"probability":0.017699115044248,"occurences":220},"act":{"probability":0.0088495575221239,"occurences":110},"I":{"probability":0.12389380530973,"occurences":1540},"our":{"probability":0.0088495575221239,"occurences":110},"helping":{"probability":0.0088495575221239,"occurences":110},"husband.":{"probability":0.0088495575221239,"occurences":110},"thoughtful":{"probability":0.0088495575221239,"occurences":110},"cried.":{"probability":0.0088495575221239,"occurences":110},"distress\"":{"probability":0.0088495575221239,"occurences":110},"still":{"probability":0.0088495575221239,"occurences":110},"I'll":{"probability":0.0088495575221239,"occurences":110},"influential":{"probability":0.0088495575221239,"occurences":110},"to":{"probability":0.0088495575221239,"occurences":110},"charge":{"probability":0.0088495575221239,"occurences":110},"his":{"probability":0.0088495575221239,"occurences":110},"just":{"probability":0.017699115044248,"occurences":220},"I'm":{"probability":0.061946902654867,"occurences":770},"even":{"probability":0.017699115044248,"occurences":220},"what":{"probability":0.044247787610619,"occurences":550},"telling":{"probability":0.0088495575221239,"occurences":110},"inside":{"probability":0.0088495575221239,"occurences":110},"there":{"probability":0.017699115044248,"occurences":220},"it.":{"probability":0.0088495575221239,"occurences":110},"he":{"probability":0.017699115044248,"occurences":220},"they":{"probability":0.026548672566372,"occurences":330},"we":{"probability":0.026548672566372,"occurences":330},"reply":{"probability":0.017699115044248,"occurences":220},"didn't":{"probability":0.0088495575221239,"occurences":110},"that":{"probability":0.0088495575221239,"occurences":110},"with":{"probability":0.017699115044248,"occurences":220},"if":{"probability":0.0088495575221239,"occurences":110},"they're":{"probability":0.017699115044248,"occurences":220},"is":{"probability":0.0088495575221239,"occurences":110},"it":{"probability":0.044247787610619,"occurences":550},"make":{"probability":0.0088495575221239,"occurences":110},"cried":{"probability":0.017699115044248,"occurences":220},"her":{"probability":0.0088495575221239,"occurences":110},"know":{"probability":0.0088495575221239,"occurences":110},"you're":{"probability":0.017699115044248,"occurences":220},"before":{"probability":0.0088495575221239,"occurences":110},"my":{"probability":0.0088495575221239,"occurences":110},"get":{"probability":0.0088495575221239,"occurences":110}},"most":{"part,":{"probability":0.33333333333333,"occurences":3},"amazing":{"probability":0.33333333333333,"occurences":3},"powerful":{"probability":0.33333333333333,"occurences":3}},"kid.":{"I'm":{"probability":1,"occurences":4}},"we're":{"still":{"probability":0.5,"occurences":2},"useless":{"probability":0.5,"occurences":2}},"makes":{"the":{"probability":0.2,"occurences":5},"me":{"probability":0.2,"occurences":5},"us":{"probability":0.4,"occurences":10},"mistakes.":{"probability":0.2,"occurences":5}},"achieve":{"them.":{"probability":1,"occurences":1}},"everything's":{"going.":{"probability":1,"occurences":1}},"saddle":{"and":{"probability":1,"occurences":1}},"He's":{"much":{"probability":1,"occurences":1}},"\"change\"":{"here.":{"probability":1,"occurences":1}},"when":{"I":{"probability":0.28571428571429,"occurences":14},"he":{"probability":0.14285714285714,"occurences":7},"she":{"probability":0.14285714285714,"occurences":7},"the":{"probability":0.14285714285714,"occurences":7},"they":{"probability":0.14285714285714,"occurences":7},"we":{"probability":0.14285714285714,"occurences":7}},"taking":{"the":{"probability":1,"occurences":9}},"techs.":{"He":{"probability":1,"occurences":1}},"married":{"but":{"probability":0.125,"occurences":8},"men":{"probability":0.125,"occurences":8},"and":{"probability":0.125,"occurences":8},"men,":{"probability":0.125,"occurences":8},"man":{"probability":0.125,"occurences":8},"who":{"probability":0.125,"occurences":8},"for":{"probability":0.125,"occurences":8},"--":{"probability":0.125,"occurences":8}},"saw":{"her":{"probability":0.5,"occurences":2},"each":{"probability":0.5,"occurences":2}},"For":{"the":{"probability":1,"occurences":1}},"realizes":{"how":{"probability":1,"occurences":1}},"guys":{"they":{"probability":0.5,"occurences":2},"from":{"probability":0.5,"occurences":2}},"learn":{"what":{"probability":0.25,"occurences":4},"that":{"probability":0.25,"occurences":4},"something":{"probability":0.25,"occurences":4},"how":{"probability":0.25,"occurences":4}},"go.":{"I":{"probability":0.5,"occurences":1},"The":{"probability":0.5,"occurences":1}},"raise.":{"The":{"probability":0.5,"occurences":2},"She":{"probability":0.5,"occurences":2}},"building":{"things.":{"probability":1,"occurences":1}},"yes,":{"I":{"probability":1,"occurences":1}},"seems":{"like":{"probability":0.5,"occurences":2},"that":{"probability":0.5,"occurences":2}},"smart.":{"You":{"probability":1,"occurences":1}},"before":{"they":{"probability":1,"occurences":1}},"getting":{"a":{"probability":0.5,"occurences":2},"out":{"probability":0.5,"occurences":2}},"people":{"say":{"probability":0.2,"occurences":125},"here.":{"probability":0.04,"occurences":25},"who":{"probability":0.16,"occurences":100},"go":{"probability":0.04,"occurences":25},"here":{"probability":0.04,"occurences":25},"think":{"probability":0.52,"occurences":325}},"strong.":{"You":{"probability":1,"occurences":1}},"caring":{"person.":{"probability":1,"occurences":1}},"confused":{"about":{"probability":1,"occurences":1}},"parent.":{"I":{"probability":0.8,"occurences":20},"A":{"probability":0.2,"occurences":5}},"And":{"I'm":{"probability":0.5,"occurences":8},"if":{"probability":0.25,"occurences":4},"it":{"probability":0.25,"occurences":4}},"powerful":{"and":{"probability":1,"occurences":1}},"how":{"you":{"probability":0.125,"occurences":8},"hard":{"probability":0.125,"occurences":8},"to":{"probability":0.625,"occurences":40},"it":{"probability":0.125,"occurences":8}},"look":{"like":{"probability":1,"occurences":1}},"else.":{"I":{"probability":1,"occurences":1}},"so":{"heavy":{"probability":0.032258064516129,"occurences":27},"much":{"probability":0.032258064516129,"occurences":27},"well.":{"probability":0.032258064516129,"occurences":27},"hard":{"probability":0.032258064516129,"occurences":27},"little":{"probability":0.032258064516129,"occurences":27},"happy":{"probability":0.032258064516129,"occurences":27},"don't":{"probability":0.032258064516129,"occurences":27},"difficult.":{"probability":0.032258064516129,"occurences":27},"confused":{"probability":0.032258064516129,"occurences":27},"ecstatic":{"probability":0.032258064516129,"occurences":27},"fucking":{"probability":0.064516129032258,"occurences":54},"sad.":{"probability":0.032258064516129,"occurences":27},"funny.":{"probability":0.032258064516129,"occurences":27},"know":{"probability":0.032258064516129,"occurences":27},"much.":{"probability":0.032258064516129,"occurences":27},"angry?":{"probability":0.032258064516129,"occurences":27},"I":{"probability":0.064516129032258,"occurences":54},"lost":{"probability":0.032258064516129,"occurences":27},"thankful.":{"probability":0.032258064516129,"occurences":27},"that":{"probability":0.096774193548387,"occurences":81},"scared.":{"probability":0.032258064516129,"occurences":27},"grateful":{"probability":0.032258064516129,"occurences":27},"long":{"probability":0.064516129032258,"occurences":54},"ask":{"probability":0.032258064516129,"occurences":27},"a":{"probability":0.032258064516129,"occurences":27},"frustrating":{"probability":0.032258064516129,"occurences":27}},"rider":{"from":{"probability":0.5,"occurences":2},"so":{"probability":0.5,"occurences":2}},"an":{"their":{"probability":0.027777777777778,"occurences":7},"who":{"probability":0.083333333333333,"occurences":21},"work":{"probability":0.027777777777778,"occurences":7},"in":{"probability":0.027777777777778,"occurences":7},"talk":{"probability":0.027777777777778,"occurences":7},"be.":{"probability":0.027777777777778,"occurences":7},"'all":{"probability":0.027777777777778,"occurences":7},"amazing":{"probability":0.027777777777778,"occurences":7},"his":{"probability":0.027777777777778,"occurences":7},"of":{"probability":0.027777777777778,"occurences":7},"by":{"probability":0.027777777777778,"occurences":7},"being":{"probability":0.027777777777778,"occurences":7},"study":{"probability":0.027777777777778,"occurences":7},"say":{"probability":0.027777777777778,"occurences":7},"for":{"probability":0.027777777777778,"occurences":7},"do":{"probability":0.027777777777778,"occurences":7},"or":{"probability":0.11111111111111,"occurences":28},"is":{"probability":0.055555555555556,"occurences":14},"actual":{"probability":0.027777777777778,"occurences":7},"remain":{"probability":0.027777777777778,"occurences":7},"amateur":{"probability":0.027777777777778,"occurences":7},"make":{"probability":0.027777777777778,"occurences":7},"when":{"probability":0.055555555555556,"occurences":14},"atheist.":{"probability":0.027777777777778,"occurences":7},"expert":{"probability":0.027777777777778,"occurences":7},"learn":{"probability":0.027777777777778,"occurences":7},"English":{"probability":0.027777777777778,"occurences":7},"with":{"probability":0.027777777777778,"occurences":7},"that":{"probability":0.027777777777778,"occurences":7}},"friend":{"and":{"probability":0.5,"occurences":1},"who":{"probability":0.5,"occurences":1}},"blogs,":{"social":{"probability":1,"occurences":4}},"it.":{"I":{"probability":0.14285714285714,"occurences":7},"If":{"probability":0.28571428571429,"occurences":14},"It's":{"probability":0.14285714285714,"occurences":7},"Thank":{"probability":0.14285714285714,"occurences":7},"So,":{"probability":0.14285714285714,"occurences":7},"Yeah":{"probability":0.14285714285714,"occurences":7}},"habit":{"of":{"probability":1,"occurences":1}},"work,":{"how":{"probability":0.5,"occurences":2},"go":{"probability":0.5,"occurences":2}},"(or":{"'tumblr')":{"probability":0.66666666666667,"occurences":6},"'tumblrs')":{"probability":0.33333333333333,"occurences":3}},"marriage":{"more":{"probability":1,"occurences":1}},"lawyer":{"appealed":{"probability":1,"occurences":1}},"happening":{"here":{"probability":1,"occurences":1}},"fat":{"kid.":{"probability":1,"occurences":1}},"do.":{"I":{"probability":0.33333333333333,"occurences":3},"It's":{"probability":0.33333333333333,"occurences":3},"You":{"probability":0.33333333333333,"occurences":3}},"posts.":{"So,":{"probability":1,"occurences":1}},"relationship":{"with":{"probability":0.5,"occurences":2},"is":{"probability":0.5,"occurences":2}},"life,":{"and":{"probability":1,"occurences":1}},"sorry":{"for":{"probability":0.33333333333333,"occurences":3},"that":{"probability":0.33333333333333,"occurences":3},"your":{"probability":0.33333333333333,"occurences":3}},"job,":{"you":{"probability":0.33333333333333,"occurences":3},"how":{"probability":0.33333333333333,"occurences":3},"or":{"probability":0.33333333333333,"occurences":3}},"here.":{"You":{"probability":0.5,"occurences":4},"I":{"probability":0.5,"occurences":4}},"learning,":{"I":{"probability":1,"occurences":1}},"000,":{"plus":{"probability":1,"occurences":1}},"work.":{"You":{"probability":1,"occurences":1}},"king,":{"he":{"probability":1,"occurences":1}},"full":{"of":{"probability":1,"occurences":4}},"case.":{"The":{"probability":1,"occurences":4}},"burn.":{"I'm":{"probability":1,"occurences":1}},"worked":{"hard":{"probability":1,"occurences":1}},"women":{"I":{"probability":0.16666666666667,"occurences":6},"you're":{"probability":0.16666666666667,"occurences":6},"who":{"probability":0.66666666666667,"occurences":24}},"This":{"past":{"probability":0.33333333333333,"occurences":3},"is":{"probability":0.33333333333333,"occurences":3},"sub":{"probability":0.33333333333333,"occurences":3}},"talented.":{"You":{"probability":1,"occurences":1}},"past.":{"You":{"probability":1,"occurences":1}},"So":{"yesterday":{"probability":0.2,"occurences":5},"I":{"probability":0.4,"occurences":10},"my":{"probability":0.2,"occurences":5},"yes,":{"probability":0.2,"occurences":5}},"point":{"of":{"probability":0.33333333333333,"occurences":3},"where":{"probability":0.33333333333333,"occurences":3},"is":{"probability":0.33333333333333,"occurences":3}},"again":{"are":{"probability":1,"occurences":1}},"though,":{"that":{"probability":1,"occurences":1}},"engineering":{"so":{"probability":1,"occurences":1}},"loved.":{"He":{"probability":1,"occurences":1}},"figure":{"out":{"probability":1,"occurences":4}},"joke":{"group":{"probability":1,"occurences":1}},"'harassment'.":{"TIL":{"probability":1,"occurences":1}},"fate.":{"I'm":{"probability":1,"occurences":1}},"record":{"who":{"probability":1,"occurences":1}},"tumblrites":{"are":{"probability":1,"occurences":4}},"issues":{"with":{"probability":1,"occurences":4}},"entitled.":{"You":{"probability":1,"occurences":1}},"drama":{"queen,":{"probability":1,"occurences":1}},"forums.":{"There":{"probability":1,"occurences":4}},"person.":{"I":{"probability":0.72727272727273,"occurences":352},"He":{"probability":0.045454545454545,"occurences":22},"Because":{"probability":0.045454545454545,"occurences":22},"You":{"probability":0.090909090909091,"occurences":44},"It's":{"probability":0.045454545454545,"occurences":22},"What":{"probability":0.045454545454545,"occurences":22}},"girl":{"in":{"probability":0.33333333333333,"occurences":3},"that":{"probability":0.33333333333333,"occurences":3},"with":{"probability":0.33333333333333,"occurences":3}},"solid.":{"He's":{"probability":1,"occurences":1}},"honest":{"man.":{"probability":1,"occurences":1}},"can":{"say":{"probability":0.11111111111111,"occurences":9},"do":{"probability":0.11111111111111,"occurences":9},"learn":{"probability":0.11111111111111,"occurences":9},"talk":{"probability":0.11111111111111,"occurences":9},"remain":{"probability":0.11111111111111,"occurences":9},"make":{"probability":0.11111111111111,"occurences":9},"work":{"probability":0.11111111111111,"occurences":9},"be.":{"probability":0.11111111111111,"occurences":9},"study":{"probability":0.11111111111111,"occurences":9}},"verses":{"as":{"probability":1,"occurences":1}},"open":{"the":{"probability":0.25,"occurences":4},"field,":{"probability":0.25,"occurences":4},"to":{"probability":0.25,"occurences":4},"it":{"probability":0.25,"occurences":4}},"mind":{"when":{"probability":1,"occurences":1}},"mine.":{"He":{"probability":1,"occurences":1}},"looks":{"like.":{"probability":1,"occurences":1}},"him!":{"I'm":{"probability":1,"occurences":1}},"If":{"I":{"probability":0.16666666666667,"occurences":5},"you're":{"probability":0.33333333333333,"occurences":10},"they":{"probability":0.16666666666667,"occurences":5},"you":{"probability":0.33333333333333,"occurences":10}},"$15":{"000,":{"probability":0.5,"occurences":2},"000.":{"probability":0.5,"occurences":2}},"life":{"sucks,":{"probability":0.2,"occurences":5},"better.":{"probability":0.2,"occurences":5},"back?":{"probability":0.2,"occurences":5},"in":{"probability":0.2,"occurences":5},"so":{"probability":0.2,"occurences":5}},"spend":{"all":{"probability":1,"occurences":1}},"why":{"you":{"probability":0.66666666666667,"occurences":6},"are":{"probability":0.33333333333333,"occurences":3}},"settlement":{"will":{"probability":1,"occurences":1}},"to.":{"People":{"probability":0.5,"occurences":2},"I":{"probability":0.5,"occurences":2}},"mod":{"to":{"probability":1,"occurences":1}},"money.":{"You":{"probability":1,"occurences":1}},"sub":{"is":{"probability":1,"occurences":1}},"only":{"way":{"probability":0.33333333333333,"occurences":3},"reason":{"probability":0.33333333333333,"occurences":3},"women":{"probability":0.33333333333333,"occurences":3}},"copyright":{"infringement.":{"probability":1,"occurences":1}},"fan;":{"calling":{"probability":1,"occurences":1}},"you.":{"You":{"probability":0.33333333333333,"occurences":3},"That's":{"probability":0.33333333333333,"occurences":3},"You're":{"probability":0.33333333333333,"occurences":3}},"say":{"they":{"probability":0.125,"occurences":8},"that":{"probability":0.875,"occurences":56}},"scared.":{"I":{"probability":0.5,"occurences":2},"I'm":{"probability":0.5,"occurences":2}},"granted":{"to":{"probability":1,"occurences":1}},"armed":{"in":{"probability":0.3,"occurences":30},"with":{"probability":0.7,"occurences":70}},"way":{"is":{"probability":1,"occurences":1}},"house":{"and":{"probability":0.5,"occurences":2},"today":{"probability":0.5,"occurences":2}},"knew":{"she":{"probability":0.5,"occurences":2},"each":{"probability":0.5,"occurences":2}},"queen,":{"he":{"probability":1,"occurences":1}},"ones":{"who":{"probability":1,"occurences":1}},"creators":{"of":{"probability":1,"occurences":1}},"government":{"is":{"probability":1,"occurences":4}},"kind":{"of":{"probability":1,"occurences":4}},"which":{"were":{"probability":0.33333333333333,"occurences":3},"is":{"probability":0.33333333333333,"occurences":3},"at":{"probability":0.33333333333333,"occurences":3}},"blog":{"of":{"probability":0.6,"occurences":15},"is":{"probability":0.2,"occurences":5},"and":{"probability":0.2,"occurences":5}},"like,":{"what":{"probability":1,"occurences":1}},"myself":{"anymore.":{"probability":0.5,"occurences":2},"to":{"probability":0.5,"occurences":2}}} \ No newline at end of file diff --git a/resources/travisscott.json b/resources/travisscott.json new file mode 100644 index 0000000..0d176b7 --- /dev/null +++ b/resources/travisscott.json @@ -0,0 +1 @@ +{"perhaps":{"even":{"probability":1,"occurences":1}},"where":{"the":{"probability":0.5,"occurences":15},"are":{"probability":0.16666666666667,"occurences":5},"to":{"probability":0.16666666666667,"occurences":5},"politics":{"probability":0.16666666666667,"occurences":5}},"coherent":{"index":{"probability":1,"occurences":1}},"future":{"presidents":{"probability":0.14285714285714,"occurences":7},"today's":{"probability":0.14285714285714,"occurences":7},"becomes":{"probability":0.14285714285714,"occurences":7},"means":{"probability":0.14285714285714,"occurences":7},"is":{"probability":0.14285714285714,"occurences":7},"operationality":{"probability":0.28571428571429,"occurences":14}},"mcdonald's":{"is":{"probability":0.27272727272727,"occurences":33},"wants":{"probability":0.090909090909091,"occurences":11},"play":{"probability":0.090909090909091,"occurences":11},"can":{"probability":0.090909090909091,"occurences":11},"isn't":{"probability":0.090909090909091,"occurences":11},"which":{"probability":0.090909090909091,"occurences":11},"collab":{"probability":0.090909090909091,"occurences":11},"burger":{"probability":0.090909090909091,"occurences":11},"this":{"probability":0.090909090909091,"occurences":11}},"pretend":{"at":{"probability":1,"occurences":1}},"particular":{"given":{"probability":0.33333333333333,"occurences":3},"are":{"probability":0.33333333333333,"occurences":3},"rift":{"probability":0.33333333333333,"occurences":3}},"implicit":{"expectation":{"probability":1,"occurences":1}},"days":{"are":{"probability":1,"occurences":1}},"though":{"is":{"probability":1,"occurences":1}},"began":{"it":{"probability":1,"occurences":1}},"much":{"unique":{"probability":0.2,"occurences":5},"it's":{"probability":0.2,"occurences":5},"from":{"probability":0.2,"occurences":5},"outside":{"probability":0.2,"occurences":5},"and":{"probability":0.2,"occurences":5}},"almost":{"a":{"probability":1,"occurences":1}},"special":{"news":{"probability":1,"occurences":1}},"even":{"predatory":{"probability":1,"occurences":1}},"steep":{"age":{"probability":1,"occurences":1}},"sometimes":{"the":{"probability":1,"occurences":1}},"positive":{"associations":{"probability":0.5,"occurences":2},"interference":{"probability":0.5,"occurences":2}},"doesn't":{"talk":{"probability":0.2,"occurences":5},"stray":{"probability":0.2,"occurences":5},"add":{"probability":0.2,"occurences":5},"interview":{"probability":0.2,"occurences":5},"matter":{"probability":0.2,"occurences":5}},"well-trodden":{"yarns":{"probability":1,"occurences":1}},"tomorrow":{"based":{"probability":1,"occurences":1}},"love":{"this":{"probability":1,"occurences":1}},"it":{"to":{"probability":0.052631578947368,"occurences":12},"means":{"probability":0.052631578947368,"occurences":12},"universally":{"probability":0.052631578947368,"occurences":12},"exploits":{"probability":0.052631578947368,"occurences":12},"of":{"probability":0.052631578947368,"occurences":12},"this":{"probability":0.052631578947368,"occurences":12},"you":{"probability":0.10526315789474,"occurences":24},"should":{"probability":0.052631578947368,"occurences":12},"with":{"probability":0.052631578947368,"occurences":12},"wants":{"probability":0.052631578947368,"occurences":12},"my":{"probability":0.052631578947368,"occurences":12},"that's":{"probability":0.052631578947368,"occurences":12},"mean":{"probability":0.052631578947368,"occurences":12},"now":{"probability":0.052631578947368,"occurences":12},"which":{"probability":0.052631578947368,"occurences":12},"expectation":{"probability":0.052631578947368,"occurences":12},"wasn't":{"probability":0.052631578947368,"occurences":12},"and":{"probability":0.052631578947368,"occurences":12}},"observation":{"in":{"probability":1,"occurences":1}},"same":{"year":{"probability":0.5,"occurences":2},"reason":{"probability":0.5,"occurences":2}},"new":{"media":{"probability":0.2,"occurences":5},"age":{"probability":0.2,"occurences":5},"only":{"probability":0.2,"occurences":5},"kind":{"probability":0.4,"occurences":10}},"create":{"this":{"probability":1,"occurences":1}},"what's":{"been":{"probability":1,"occurences":1}},"associations":{"it's":{"probability":0.5,"occurences":2},"you":{"probability":0.5,"occurences":2}},"whom":{"i":{"probability":0.5,"occurences":2},"have":{"probability":0.5,"occurences":2}},"golden":{"temple":{"probability":1,"occurences":1}},"nothing":{"more":{"probability":1,"occurences":1}},"lit":{"which":{"probability":0.25,"occurences":4},"now":{"probability":0.25,"occurences":4},"you":{"probability":0.25,"occurences":4},"this":{"probability":0.25,"occurences":4}},"like":{"some":{"probability":0.25,"occurences":3},"this":{"probability":0.75,"occurences":9}},"reversing":{"into":{"probability":1,"occurences":1}},"cool":{"fun":{"probability":1,"occurences":1}},"prospects":{"and":{"probability":1,"occurences":1}},"deterioration":{"of":{"probability":1,"occurences":1}},"be":{"a":{"probability":0.5,"occurences":8},"sold":{"probability":0.25,"occurences":4},"some":{"probability":0.25,"occurences":4}},"nowhere":{"to":{"probability":1,"occurences":1}},"surface":{"or":{"probability":1,"occurences":1}},"back":{"my":{"probability":0.33333333333333,"occurences":3},"here":{"probability":0.66666666666667,"occurences":6}},"isn't":{"asking":{"probability":1,"occurences":1}},"concerned":{"while":{"probability":1,"occurences":1}},"transparency":{"guys":{"probability":0.5,"occurences":2},"means":{"probability":0.5,"occurences":2}},"sponsored":{"community":{"probability":1,"occurences":1}},"course":{"is":{"probability":0.16666666666667,"occurences":6},"of":{"probability":0.16666666666667,"occurences":6},"fire":{"probability":0.16666666666667,"occurences":6},"a":{"probability":0.33333333333333,"occurences":12},"it":{"probability":0.16666666666667,"occurences":6}},"anyway":{"and":{"probability":1,"occurences":1}},"where's":{"he":{"probability":1,"occurences":1}},"celebrities":{"generally":{"probability":0.5,"occurences":2},"lend":{"probability":0.5,"occurences":2}},"goddamn":{"ingredient":{"probability":1,"occurences":1}},"missed":{"the":{"probability":1,"occurences":1}},"pretty":{"steep":{"probability":0.33333333333333,"occurences":3},"down":{"probability":0.33333333333333,"occurences":3},"[":{"probability":0.33333333333333,"occurences":3}},"parallel":{"universe":{"probability":1,"occurences":1}},"else":{"is":{"probability":1,"occurences":1}},"cause":{"the":{"probability":0.33333333333333,"occurences":1},"you":{"probability":0.33333333333333,"occurences":1},"or":{"probability":0.33333333333333,"occurences":1}},"precisely":{"the":{"probability":1,"occurences":1}},"photograph":{"the":{"probability":1,"occurences":1}},"persona":{"a":{"probability":1,"occurences":1}},"transcendent":{"flying":{"probability":1,"occurences":1}},"radically":{"different":{"probability":1,"occurences":1}},"manner":{"in":{"probability":1,"occurences":1}},"hyperreal":{"zodiac":{"probability":1,"occurences":4}},"universe":{"began":{"probability":0.5,"occurences":2},"where":{"probability":0.5,"occurences":2}},"than":{"the":{"probability":1,"occurences":1}},"that":{"i":{"probability":0.071428571428571,"occurences":14},"was":{"probability":0.071428571428571,"occurences":14},"however":{"probability":0.071428571428571,"occurences":14},"is":{"probability":0.071428571428571,"occurences":14},"it":{"probability":0.071428571428571,"occurences":14},"the":{"probability":0.14285714285714,"occurences":28},"enables":{"probability":0.071428571428571,"occurences":14},"it's":{"probability":0.071428571428571,"occurences":14},"left":{"probability":0.071428571428571,"occurences":14},"travis":{"probability":0.071428571428571,"occurences":14},"right":{"probability":0.071428571428571,"occurences":14},"you":{"probability":0.071428571428571,"occurences":14},"we":{"probability":0.071428571428571,"occurences":14}},"child":{"one":{"probability":1,"occurences":1}},"pull":{"off":{"probability":1,"occurences":1}},"coupled":{"with":{"probability":1,"occurences":1}},"sensible":{"confluence":{"probability":1,"occurences":1}},"capital":{"has":{"probability":1,"occurences":1}},"management":{"yeah":{"probability":1,"occurences":1}},"chapel":{"of":{"probability":1,"occurences":1}},"but":{"go":{"probability":0.090909090909091,"occurences":11},"way":{"probability":0.090909090909091,"occurences":11},"just":{"probability":0.090909090909091,"occurences":11},"it":{"probability":0.090909090909091,"occurences":11},"the":{"probability":0.090909090909091,"occurences":11},"it's":{"probability":0.090909090909091,"occurences":11},"where's":{"probability":0.090909090909091,"occurences":11},"what":{"probability":0.090909090909091,"occurences":11},"there's":{"probability":0.090909090909091,"occurences":11},"a":{"probability":0.090909090909091,"occurences":11},"this":{"probability":0.090909090909091,"occurences":11}},"rappers":{"in":{"probability":1,"occurences":1}},"post-modernisms":{"so":{"probability":1,"occurences":1}},"recognition":{"drops":{"probability":1,"occurences":1}},"my":{"childhood":{"probability":1,"occurences":4}},"came":{"to":{"probability":1,"occurences":1}},"significations":{"but":{"probability":1,"occurences":1}},"feature":{"of":{"probability":1,"occurences":1}},"origins":{"in":{"probability":1,"occurences":1}},"second":{"celebrities":{"probability":1,"occurences":1}},"let's":{"place":{"probability":0.5,"occurences":2},"be":{"probability":0.5,"occurences":2}},"toy":{"anymore":{"probability":1,"occurences":1}},"overcode":{"all":{"probability":1,"occurences":1}},"here":{"seems":{"probability":0.043478260869565,"occurences":11},"for":{"probability":0.08695652173913,"occurences":22},"to":{"probability":0.08695652173913,"occurences":22},"are":{"probability":0.043478260869565,"occurences":11},"is":{"probability":0.17391304347826,"occurences":44},"politics":{"probability":0.043478260869565,"occurences":11},"the":{"probability":0.17391304347826,"occurences":44},"paris":{"probability":0.043478260869565,"occurences":11},"but":{"probability":0.043478260869565,"occurences":11},"now":{"probability":0.08695652173913,"occurences":22},"it'll":{"probability":0.043478260869565,"occurences":11},"travis":{"probability":0.043478260869565,"occurences":11},"jordan":{"probability":0.043478260869565,"occurences":11},"has":{"probability":0.043478260869565,"occurences":11}},"hundreds":{"of":{"probability":1,"occurences":1}},"about":{"luxury":{"probability":0.5,"occurences":2},"much":{"probability":0.5,"occurences":2}},"just":{"in":{"probability":0.5,"occurences":2},"get":{"probability":0.5,"occurences":2}},"whereas":{"marx":{"probability":1,"occurences":1}},"The":{"past":{"probability":0.2,"occurences":5},"sound":{"probability":0.2,"occurences":5},"merch":{"probability":0.2,"occurences":5},"goal":{"probability":0.2,"occurences":5},"supreme":{"probability":0.2,"occurences":5}},"activism":{"and":{"probability":1,"occurences":1}},"if":{"you're":{"probability":0.5,"occurences":2},"your":{"probability":0.5,"occurences":2}},"of":{"reference":{"probability":0.019607843137255,"occurences":51},"hype":{"probability":0.019607843137255,"occurences":51},"significations":{"probability":0.019607843137255,"occurences":51},"the":{"probability":0.07843137254902,"occurences":204},"consumer":{"probability":0.019607843137255,"occurences":51},"nostalgia":{"probability":0.019607843137255,"occurences":51},"whom":{"probability":0.019607843137255,"occurences":51},"an":{"probability":0.019607843137255,"occurences":51},"this":{"probability":0.058823529411765,"occurences":153},"fame":{"probability":0.019607843137255,"occurences":51},"ingredients":{"probability":0.019607843137255,"occurences":51},"all":{"probability":0.07843137254902,"occurences":204},"travis":{"probability":0.058823529411765,"occurences":153},"post":{"probability":0.019607843137255,"occurences":51},"workers":{"probability":0.019607843137255,"occurences":51},"celebrity":{"probability":0.019607843137255,"occurences":51},"his":{"probability":0.019607843137255,"occurences":51},"anything":{"probability":0.019607843137255,"occurences":51},"social":{"probability":0.019607843137255,"occurences":51},"superficiality":{"probability":0.019607843137255,"occurences":51},"transparency":{"probability":0.019607843137255,"occurences":51},"course":{"probability":0.11764705882353,"occurences":306},"us":{"probability":0.019607843137255,"occurences":51},"a":{"probability":0.07843137254902,"occurences":204},"signification":{"probability":0.019607843137255,"occurences":51},"flatness":{"probability":0.019607843137255,"occurences":51},"scott's":{"probability":0.019607843137255,"occurences":51},"thousands":{"probability":0.019607843137255,"occurences":51},"these":{"probability":0.019607843137255,"occurences":51},"which":{"probability":0.019607843137255,"occurences":51},"music":{"probability":0.019607843137255,"occurences":51},"ethereality":{"probability":0.019607843137255,"occurences":51},"global":{"probability":0.019607843137255,"occurences":51}},"black":{"hole":{"probability":1,"occurences":1}},"currently":{"live":{"probability":1,"occurences":1}},"don't":{"own":{"probability":0.5,"occurences":2},"take":{"probability":0.5,"occurences":2}},"every":{"level":{"probability":0.66666666666667,"occurences":6},"celebrity":{"probability":0.33333333333333,"occurences":3}},"however":{"i'm":{"probability":0.5,"occurences":2},"there":{"probability":0.5,"occurences":2}},"flatness":{"or":{"probability":1,"occurences":1}},"anymore":{"you":{"probability":0.5,"occurences":2},"get":{"probability":0.5,"occurences":2}},"bullet":{"for":{"probability":1,"occurences":1}},"increasingly":{"uncertain":{"probability":0.5,"occurences":2},"attractive":{"probability":0.5,"occurences":2}},"given":{"raps":{"probability":1,"occurences":1}},"universal":{"transparency":{"probability":1,"occurences":1}},"does":{"it":{"probability":1,"occurences":1}},"in":{"any":{"probability":0.052631578947368,"occurences":17},"that":{"probability":0.10526315789474,"occurences":34},"and":{"probability":0.052631578947368,"occurences":17},"in":{"probability":0.052631578947368,"occurences":17},"particular":{"probability":0.10526315789474,"occurences":34},"between":{"probability":0.052631578947368,"occurences":17},"the":{"probability":0.21052631578947,"occurences":68},"protest":{"probability":0.052631578947368,"occurences":17},"which":{"probability":0.15789473684211,"occurences":51},"case":{"probability":0.052631578947368,"occurences":17},"their":{"probability":0.052631578947368,"occurences":17},"under":{"probability":0.052631578947368,"occurences":17}},"aforementioned":{"fire":{"probability":1,"occurences":1}},"on":{"except":{"probability":0.032258064516129,"occurences":7},"that":{"probability":0.032258064516129,"occurences":7},"to":{"probability":0.032258064516129,"occurences":7},"in":{"probability":0.096774193548387,"occurences":21},"on":{"probability":0.032258064516129,"occurences":7},"the":{"probability":0.064516129032258,"occurences":14},"then":{"probability":0.032258064516129,"occurences":7},"there":{"probability":0.032258064516129,"occurences":7},"of":{"probability":0.12903225806452,"occurences":28},"for":{"probability":0.064516129032258,"occurences":14},"rather":{"probability":0.032258064516129,"occurences":7},"every":{"probability":0.032258064516129,"occurences":7},"drops":{"probability":0.032258064516129,"occurences":7},"is":{"probability":0.032258064516129,"occurences":7},"and":{"probability":0.032258064516129,"occurences":7},"called":{"probability":0.032258064516129,"occurences":7},"it's":{"probability":0.032258064516129,"occurences":7},"now":{"probability":0.032258064516129,"occurences":7},"refers":{"probability":0.032258064516129,"occurences":7},"themselves":{"probability":0.032258064516129,"occurences":7},"a":{"probability":0.096774193548387,"occurences":21},"claims":{"probability":0.032258064516129,"occurences":7}},"paris":{"hilton":{"probability":1,"occurences":1}},"apart":{"is":{"probability":1,"occurences":1}},"an":{"and":{"probability":0.047619047619048,"occurences":7},"ambiguity":{"probability":0.047619047619048,"occurences":7},"see":{"probability":0.047619047619048,"occurences":7},"first":{"probability":0.047619047619048,"occurences":7},"the":{"probability":0.095238095238095,"occurences":14},"image":{"probability":0.047619047619048,"occurences":7},"appropriate":{"probability":0.047619047619048,"occurences":7},"invest":{"probability":0.047619047619048,"occurences":7},"was":{"probability":0.047619047619048,"occurences":7},"bet":{"probability":0.047619047619048,"occurences":7},"it":{"probability":0.047619047619048,"occurences":7},"make":{"probability":0.047619047619048,"occurences":7},"intoxicating":{"probability":0.047619047619048,"occurences":7},"align":{"probability":0.047619047619048,"occurences":7},"implicit":{"probability":0.047619047619048,"occurences":7},"overcode":{"probability":0.047619047619048,"occurences":7},"already":{"probability":0.095238095238095,"occurences":14},"amusement":{"probability":0.047619047619048,"occurences":7},"ambience":{"probability":0.047619047619048,"occurences":7}},"drake":{"on":{"probability":1,"occurences":1}},"ingredients":{"in":{"probability":1,"occurences":1}},"i'm":{"roughly":{"probability":0.5,"occurences":2},"here":{"probability":0.5,"occurences":2}},"amazing":{"really":{"probability":1,"occurences":1}},"via":{"the":{"probability":0.5,"occurences":2},"travis":{"probability":0.5,"occurences":2}},"acceptance":{"of":{"probability":1,"occurences":1}},"uncertain":{"and":{"probability":1,"occurences":1}},"images":{"are":{"probability":0.5,"occurences":2},"like":{"probability":0.5,"occurences":2}},"meal":{"when":{"probability":0.5,"occurences":2},"toy":{"probability":0.5,"occurences":2}},"anything":{"because":{"probability":0.5,"occurences":2},"we":{"probability":0.5,"occurences":2}},"purpose":{"of":{"probability":1,"occurences":1}},"last":{"biggest":{"probability":0.33333333333333,"occurences":3},"to":{"probability":0.33333333333333,"occurences":3},"burger":{"probability":0.33333333333333,"occurences":3}},"context":{"of":{"probability":1,"occurences":1}},"kardashian":{"and":{"probability":1,"occurences":1}},"hole":{"weaving":{"probability":0.16666666666667,"occurences":1},"web":{"probability":0.16666666666667,"occurences":1},"collaboration":{"probability":0.16666666666667,"occurences":1},"persona":{"probability":0.16666666666667,"occurences":1},"mode":{"probability":0.16666666666667,"occurences":1},"thing":{"probability":0.16666666666667,"occurences":1}},"happy":{"meals":{"probability":0.5,"occurences":2},"meal":{"probability":0.5,"occurences":2}},"case":{"what":{"probability":0.5,"occurences":2},"you've":{"probability":0.5,"occurences":2}},"politically":{"indifferent":{"probability":1,"occurences":1}},"non-existent":{"past":{"probability":1,"occurences":1}},"they're":{"all":{"probability":1,"occurences":1}},"attractive":{"as":{"probability":1,"occurences":1}},"index":{"and":{"probability":1,"occurences":1}},"litmus":{"The":{"probability":1,"occurences":1}},"memory":{"that":{"probability":1,"occurences":1}},"literal":{"sense":{"probability":1,"occurences":1}},"sold":{"nostalgia":{"probability":1,"occurences":1}},"becomes":{"fame":{"probability":0.25,"occurences":4},"increasingly":{"probability":0.5,"occurences":8},"colored":{"probability":0.25,"occurences":4}},"raps":{"origins":{"probability":1,"occurences":1}},"today's":{"nihilism":{"probability":1,"occurences":1}},"supreme":{"formal":{"probability":1,"occurences":1}},"left":{"nowhere":{"probability":1,"occurences":1}},"doing":{"a":{"probability":1,"occurences":1}},"because":{"the":{"probability":0.5,"occurences":2},"you":{"probability":0.5,"occurences":2}},"quarter":{"pounder":{"probability":1,"occurences":1}},"rift":{"which":{"probability":1,"occurences":1}},"stage":{"name":{"probability":1,"occurences":1}},"your":{"brain's":{"probability":0.33333333333333,"occurences":3},"fans":{"probability":0.33333333333333,"occurences":3},"head":{"probability":0.33333333333333,"occurences":3}},"two":{"in":{"probability":1,"occurences":1}},"lend":{"themselves":{"probability":1,"occurences":1}},"scott's":{"album":{"probability":0.33333333333333,"occurences":3},"name":{"probability":0.33333333333333,"occurences":3},"image":{"probability":0.33333333333333,"occurences":3}},"age":{"i":{"probability":0.05,"occurences":4},"at":{"probability":0.05,"occurences":4},"being":{"probability":0.05,"occurences":4},"the":{"probability":0.1,"occurences":8},"cliff":{"probability":0.05,"occurences":4},"this":{"probability":0.05,"occurences":4},"is":{"probability":0.1,"occurences":8},"unlike":{"probability":0.05,"occurences":4},"but":{"probability":0.05,"occurences":4},"not":{"probability":0.05,"occurences":4},"he":{"probability":0.05,"occurences":4},"apart":{"probability":0.05,"occurences":4},"of":{"probability":0.15,"occurences":12},"though":{"probability":0.05,"occurences":4},"name":{"probability":0.05,"occurences":4},"most":{"probability":0.05,"occurences":4}},"wasn't":{"here":{"probability":1,"occurences":1}},"decades":{"and":{"probability":1,"occurences":1}},"lettuce":{"to":{"probability":0.5,"occurences":2},"added":{"probability":0.5,"occurences":2}},"sententious":{"manner":{"probability":1,"occurences":1}},"appearance":{"of":{"probability":0.5,"occurences":1},"there's":{"probability":0.5,"occurences":1}},"from":{"the":{"probability":0.75,"occurences":12},"this":{"probability":0.25,"occurences":4}},"established":{"capitalist":{"probability":1,"occurences":1}},"one":{"of":{"probability":0.2,"occurences":5},"cause":{"probability":0.2,"occurences":5},"in":{"probability":0.4,"occurences":10},"which":{"probability":0.2,"occurences":5}},"mechanical":{"bird":{"probability":1,"occurences":4}},"meals":{"sponsored":{"probability":1,"occurences":1}},"added":{"to":{"probability":1,"occurences":1}},"he's":{"an":{"probability":0.25,"occurences":4},"not":{"probability":0.25,"occurences":4},"amorphous":{"probability":0.25,"occurences":4},"got":{"probability":0.25,"occurences":4}},"lost":{"the":{"probability":0.5,"occurences":2},"closed":{"probability":0.5,"occurences":2}},"enables":{"travis":{"probability":1,"occurences":1}},"try":{"to":{"probability":1,"occurences":1}},"unassuming":{"stage":{"probability":1,"occurences":1}},"birthday":{"parties":{"probability":1,"occurences":1}},"reference":{"over":{"probability":1,"occurences":1}},"guy":{"this":{"probability":1,"occurences":1}},"goal":{"for":{"probability":1,"occurences":1}},"stores":{"is":{"probability":1,"occurences":1}},"consumer":{"capitalism":{"probability":1,"occurences":4}},"do":{"was":{"probability":0.2,"occurences":5},"but":{"probability":0.4,"occurences":10},"you":{"probability":0.2,"occurences":5},"we":{"probability":0.2,"occurences":5}},"connected":{"anyway":{"probability":0.5,"occurences":2},"via":{"probability":0.5,"occurences":2}},"presidents":{"come":{"probability":1,"occurences":1}},"post":{"post-modernity":{"probability":1,"occurences":1}},"really":{"the":{"probability":0.5,"occurences":2},"changing":{"probability":0.5,"occurences":2}},"celebs":{"it":{"probability":1,"occurences":1}},"little":{"more":{"probability":1,"occurences":1}},"to":{"while":{"probability":0.028571428571429,"occurences":33},"be":{"probability":0.085714285714286,"occurences":99},"see":{"probability":0.028571428571429,"occurences":33},"create":{"probability":0.028571428571429,"occurences":33},"run":{"probability":0.028571428571429,"occurences":33},"realize":{"probability":0.028571428571429,"occurences":33},"return":{"probability":0.028571428571429,"occurences":33},"remind":{"probability":0.028571428571429,"occurences":33},"start":{"probability":0.057142857142857,"occurences":66},"any":{"probability":0.057142857142857,"occurences":66},"do":{"probability":0.057142857142857,"occurences":66},"suddenly":{"probability":0.028571428571429,"occurences":33},"conceive":{"probability":0.057142857142857,"occurences":66},"pretend":{"probability":0.028571428571429,"occurences":33},"become":{"probability":0.028571428571429,"occurences":33},"it":{"probability":0.028571428571429,"occurences":33},"as":{"probability":0.085714285714286,"occurences":99},"one":{"probability":0.028571428571429,"occurences":33},"air":{"probability":0.028571428571429,"occurences":33},"have":{"probability":0.028571428571429,"occurences":33},"the":{"probability":0.085714285714286,"occurences":99},"wait":{"probability":0.028571428571429,"occurences":33},"a":{"probability":0.057142857142857,"occurences":66},"mark":{"probability":0.028571428571429,"occurences":33}},"need":{"to":{"probability":1,"occurences":1}},"social":{"imagination":{"probability":1,"occurences":1}},"only":{"when":{"probability":0.5,"occurences":2},"serve":{"probability":0.5,"occurences":2}},"superficiality":{"in":{"probability":1,"occurences":1}},"as":{"born":{"probability":0.1,"occurences":18},"introduced":{"probability":0.05,"occurences":9},"to":{"probability":0.05,"occurences":9},"lost":{"probability":0.05,"occurences":9},"the":{"probability":0.25,"occurences":45},"marx":{"probability":0.05,"occurences":9},"reversing":{"probability":0.05,"occurences":9},"michael":{"probability":0.05,"occurences":9},"of":{"probability":0.05,"occurences":9},"worked":{"probability":0.05,"occurences":9},"a":{"probability":0.2,"occurences":36},"this":{"probability":0.05,"occurences":9}},"frederick":{"jameson":{"probability":1,"occurences":4}},"calling":{"the":{"probability":1,"occurences":1}},"restaurant":{"chain":{"probability":1,"occurences":1}},"is":{"scott":{"probability":0.17241379310345,"occurences":645},"and":{"probability":0.011494252873563,"occurences":43},"solid":{"probability":0.011494252873563,"occurences":43},"guy":{"probability":0.011494252873563,"occurences":43},"photograph":{"probability":0.011494252873563,"occurences":43},"not":{"probability":0.022988505747126,"occurences":86},"nostalgia":{"probability":0.011494252873563,"occurences":43},"an":{"probability":0.011494252873563,"occurences":43},"this":{"probability":0.022988505747126,"occurences":86},"whole":{"probability":0.011494252873563,"occurences":43},"was":{"probability":0.011494252873563,"occurences":43},"program":{"probability":0.011494252873563,"occurences":43},"particular":{"probability":0.011494252873563,"occurences":43},"outgrowth":{"probability":0.011494252873563,"occurences":43},"personality":{"probability":0.011494252873563,"occurences":43},"a":{"probability":0.022988505747126,"occurences":86},"nothing":{"probability":0.011494252873563,"occurences":43},"selling":{"probability":0.011494252873563,"occurences":43},"height":{"probability":0.011494252873563,"occurences":43},"that":{"probability":0.011494252873563,"occurences":43},"scott's":{"probability":0.022988505747126,"occurences":86},"offered":{"probability":0.011494252873563,"occurences":43},"to":{"probability":0.022988505747126,"occurences":86},"bleak":{"probability":0.011494252873563,"occurences":43},"as":{"probability":0.022988505747126,"occurences":86},"of":{"probability":0.022988505747126,"occurences":86},"hyper":{"probability":0.011494252873563,"occurences":43},"reduced":{"probability":0.011494252873563,"occurences":43},"image":{"probability":0.03448275862069,"occurences":129},"causing":{"probability":0.011494252873563,"occurences":43},"only":{"probability":0.011494252873563,"occurences":43},"there":{"probability":0.011494252873563,"occurences":43},"concerned":{"probability":0.011494252873563,"occurences":43},"universal":{"probability":0.011494252873563,"occurences":43},"black":{"probability":0.011494252873563,"occurences":43},"his":{"probability":0.011494252873563,"occurences":43},"contemporaries":{"probability":0.011494252873563,"occurences":43},"something":{"probability":0.011494252873563,"occurences":43},"one":{"probability":0.011494252873563,"occurences":43},"connected":{"probability":0.011494252873563,"occurences":43},"real":{"probability":0.011494252873563,"occurences":43},"perfect":{"probability":0.011494252873563,"occurences":43},"is":{"probability":0.13793103448276,"occurences":516},"age":{"probability":0.011494252873563,"occurences":43},"the":{"probability":0.011494252873563,"occurences":43},"hilton":{"probability":0.011494252873563,"occurences":43},"bad":{"probability":0.011494252873563,"occurences":43},"so":{"probability":0.011494252873563,"occurences":43},"entirely":{"probability":0.022988505747126,"occurences":86},"lends":{"probability":0.011494252873563,"occurences":43},"video":{"probability":0.011494252873563,"occurences":43},"most":{"probability":0.011494252873563,"occurences":43}},"protest":{"activism":{"probability":1,"occurences":1}},"totally":{"unassuming":{"probability":1,"occurences":1}},"clear":{"here":{"probability":1,"occurences":1}},"crux":{"of":{"probability":1,"occurences":1}},"real":{"event":{"probability":0.16666666666667,"occurences":4},"product":{"probability":0.16666666666667,"occurences":4},"history":{"probability":0.16666666666667,"occurences":4},"zodiac":{"probability":0.33333333333333,"occurences":8},"name":{"probability":0.16666666666667,"occurences":4}},"behind":{"us":{"probability":1,"occurences":1}},"material":{"crux":{"probability":1,"occurences":1}},"appropriate":{"this":{"probability":1,"occurences":1}},"melted":{"here":{"probability":1,"occurences":1}},"being":{"the":{"probability":0.33333333333333,"occurences":3},"metastasized":{"probability":0.33333333333333,"occurences":3},"coupled":{"probability":0.33333333333333,"occurences":3}},"recognized":{"it":{"probability":1,"occurences":1}},"whole":{"persona":{"probability":0.2,"occurences":5},"weaving":{"probability":0.2,"occurences":5},"collaboration":{"probability":0.2,"occurences":5},"mode":{"probability":0.2,"occurences":5},"thing":{"probability":0.2,"occurences":5}},"realist":{"what":{"probability":0.25,"occurences":1},"ambient":{"probability":0.25,"occurences":1},"image":{"probability":0.25,"occurences":1},"imagery":{"probability":0.25,"occurences":1}},"asking":{"for":{"probability":1,"occurences":1}},"imagination":{"the":{"probability":1,"occurences":1}},"right":{"seems":{"probability":1,"occurences":1}},"his":{"real":{"probability":0.024390243902439,"occurences":6},"offered":{"probability":0.024390243902439,"occurences":6},"and":{"probability":0.024390243902439,"occurences":6},"guy":{"probability":0.024390243902439,"occurences":6},"so":{"probability":0.024390243902439,"occurences":6},"photograph":{"probability":0.024390243902439,"occurences":6},"nostalgia":{"probability":0.024390243902439,"occurences":6},"outgrowth":{"probability":0.024390243902439,"occurences":6},"i":{"probability":0.024390243902439,"occurences":6},"as":{"probability":0.048780487804878,"occurences":12},"height":{"probability":0.024390243902439,"occurences":6},"contemporaries":{"probability":0.024390243902439,"occurences":6},"lends":{"probability":0.024390243902439,"occurences":6},"whole":{"probability":0.024390243902439,"occurences":6},"image":{"probability":0.073170731707317,"occurences":18},"was":{"probability":0.024390243902439,"occurences":6},"program":{"probability":0.024390243902439,"occurences":6},"is":{"probability":0.29268292682927,"occurences":72},"particular":{"probability":0.024390243902439,"occurences":6},"black":{"probability":0.024390243902439,"occurences":6},"fans":{"probability":0.024390243902439,"occurences":6},"personality":{"probability":0.024390243902439,"occurences":6},"universal":{"probability":0.024390243902439,"occurences":6},"only":{"probability":0.024390243902439,"occurences":6},"age":{"probability":0.024390243902439,"occurences":6},"video":{"probability":0.024390243902439,"occurences":6},"rap":{"probability":0.024390243902439,"occurences":6}},"talk":{"about":{"probability":1,"occurences":1}},"explanation":{"for":{"probability":1,"occurences":1}},"artist":{"whom":{"probability":1,"occurences":1}},"it'll":{"melt":{"probability":1,"occurences":1}},"grating":{"sometimes":{"probability":1,"occurences":1}},"based":{"on":{"probability":1,"occurences":1}},"fun":{"with":{"probability":0.5,"occurences":2},"just":{"probability":0.5,"occurences":2}},"wants":{"and":{"probability":0.5,"occurences":2},"travis":{"probability":0.5,"occurences":2}},"metastasized":{"here":{"probability":1,"occurences":1}},"air":{"jameson":{"probability":1,"occurences":1}},"nihilism":{"is":{"probability":0.5,"occurences":2},"of":{"probability":0.5,"occurences":2}},"hurting":{"your":{"probability":1,"occurences":1}},"interrupt":{"this":{"probability":1,"occurences":1}},"these":{"are":{"probability":0.5,"occurences":2},"images":{"probability":0.5,"occurences":2}},"ubiquitous":{"the":{"probability":1,"occurences":1}},"fame":{"becomes":{"probability":0.5,"occurences":2},"and":{"probability":0.5,"occurences":2}},"biggest":{"event":{"probability":1,"occurences":1}},"want":{"this":{"probability":1,"occurences":1}},"changing":{"and":{"probability":1,"occurences":1}},"up":{"to":{"probability":1,"occurences":1}},"all":{"calling":{"probability":0.11111111111111,"occurences":9},"that":{"probability":0.11111111111111,"occurences":9},"do":{"probability":0.11111111111111,"occurences":9},"new":{"probability":0.11111111111111,"occurences":9},"the":{"probability":0.22222222222222,"occurences":18},"functions":{"probability":0.11111111111111,"occurences":9},"is":{"probability":0.11111111111111,"occurences":9},"this":{"probability":0.11111111111111,"occurences":9}},"surrealist":{"ambient":{"probability":0.33333333333333,"occurences":3},"image":{"probability":0.33333333333333,"occurences":3},"imagery":{"probability":0.33333333333333,"occurences":3}},"trump":{"model":{"probability":1,"occurences":1}},"mean":{"first":{"probability":1,"occurences":1}},"celebrity":{"consumer":{"probability":0.5,"occurences":2},"could":{"probability":0.5,"occurences":2}},"plate":{"of":{"probability":1,"occurences":1}},"those":{"celebs":{"probability":0.5,"occurences":2},"40":{"probability":0.5,"occurences":2}},"who":{"don't":{"probability":1,"occurences":1}},"interview":{"much":{"probability":1,"occurences":1}},"michael":{"jordan":{"probability":1,"occurences":1}},"run":{"out":{"probability":1,"occurences":1}},"crowd":{"on":{"probability":1,"occurences":1}},"events":{"mcdonald's":{"probability":1,"occurences":1}},"jordan":{"the":{"probability":0.5,"occurences":2},"was":{"probability":0.5,"occurences":2}},"something":{"mcdonald's":{"probability":0.4,"occurences":10},"like":{"probability":0.2,"occurences":5},"nostalgia":{"probability":0.2,"occurences":5},"new":{"probability":0.2,"occurences":5}},"generated":{"lyrically":{"probability":1,"occurences":1}},"current":{"orbit":{"probability":1,"occurences":1}},"depthlessness":{"a":{"probability":1,"occurences":1}},"a":{"real":{"probability":0.016666666666667,"occurences":49},"commodity":{"probability":0.033333333333333,"occurences":98},"new":{"probability":0.05,"occurences":147},"situation":{"probability":0.016666666666667,"occurences":49},"perfectly":{"probability":0.016666666666667,"occurences":49},"future":{"probability":0.033333333333333,"occurences":98},"the":{"probability":0.016666666666667,"occurences":49},"channel":{"probability":0.016666666666667,"occurences":49},"second":{"probability":0.016666666666667,"occurences":49},"perfect":{"probability":0.033333333333333,"occurences":98},"quarter":{"probability":0.016666666666667,"occurences":49},"mcdonald's":{"probability":0.016666666666667,"occurences":49},"whole":{"probability":0.033333333333333,"occurences":98},"go":{"probability":0.033333333333333,"occurences":98},"model":{"probability":0.016666666666667,"occurences":49},"person":{"probability":0.016666666666667,"occurences":49},"giant":{"probability":0.033333333333333,"occurences":98},"travis":{"probability":0.016666666666667,"occurences":49},"partial":{"probability":0.016666666666667,"occurences":49},"mystique":{"probability":0.016666666666667,"occurences":49},"ferrari":{"probability":0.016666666666667,"occurences":49},"concert":{"probability":0.016666666666667,"occurences":49},"little":{"probability":0.016666666666667,"occurences":49},"child":{"probability":0.016666666666667,"occurences":49},"special":{"probability":0.016666666666667,"occurences":49},"look":{"probability":0.016666666666667,"occurences":49},"The":{"probability":0.016666666666667,"occurences":49},"kardashian":{"probability":0.016666666666667,"occurences":49},"luxury":{"probability":0.016666666666667,"occurences":49},"repetition":{"probability":0.016666666666667,"occurences":49},"world":{"probability":0.016666666666667,"occurences":49},"pretty":{"probability":0.016666666666667,"occurences":49},"a":{"probability":0.016666666666667,"occurences":49},"totally":{"probability":0.016666666666667,"occurences":49},"tomorrow":{"probability":0.016666666666667,"occurences":49},"burger":{"probability":0.033333333333333,"occurences":98},"and":{"probability":0.016666666666667,"occurences":49},"light":{"probability":0.016666666666667,"occurences":49},"long":{"probability":0.016666666666667,"occurences":49},"is":{"probability":0.016666666666667,"occurences":49},"criticism":{"probability":0.016666666666667,"occurences":49},"video":{"probability":0.033333333333333,"occurences":98},"mode":{"probability":0.016666666666667,"occurences":49},"past":{"probability":0.033333333333333,"occurences":98},"forms":{"probability":0.016666666666667,"occurences":49},"non-existent":{"probability":0.016666666666667,"occurences":49},"goddamn":{"probability":0.016666666666667,"occurences":49},"memory":{"probability":0.016666666666667,"occurences":49},"flat":{"probability":0.016666666666667,"occurences":49}},"least":{"a":{"probability":1,"occurences":1}},"bodriar":{"called":{"probability":1,"occurences":1}},"community":{"events":{"probability":1,"occurences":1}},"insofar":{"as":{"probability":1,"occurences":1}},"generally":{"and":{"probability":1,"occurences":1}},"locality":{"like":{"probability":1,"occurences":1}},"any":{"associations":{"probability":0.16666666666667,"occurences":5},"case":{"probability":0.16666666666667,"occurences":5},"of":{"probability":0.16666666666667,"occurences":5},"coherent":{"probability":0.16666666666667,"occurences":5},"burger":{"probability":0.16666666666667,"occurences":5},"future":{"probability":0.16666666666667,"occurences":5}},"introduced":{"to":{"probability":1,"occurences":1}},"itself":{"whereas":{"probability":1,"occurences":1}},"conceive":{"of":{"probability":1,"occurences":4}},"operationality":{"on":{"probability":0.5,"occurences":2},"at":{"probability":0.5,"occurences":2}},"person":{"rather":{"probability":1,"occurences":1}},"colored":{"with":{"probability":1,"occurences":1}},"collab":{"for":{"probability":1,"occurences":1}},"height":{"the":{"probability":1,"occurences":1}},"i":{"saw":{"probability":0.076923076923077,"occurences":11},"remember":{"probability":0.076923076923077,"occurences":11},"love":{"probability":0.076923076923077,"occurences":11},"on":{"probability":0.076923076923077,"occurences":11},"think":{"probability":0.076923076923077,"occurences":11},"didn't":{"probability":0.076923076923077,"occurences":11},"can":{"probability":0.15384615384615,"occurences":22},"was":{"probability":0.076923076923077,"occurences":11},"sure":{"probability":0.076923076923077,"occurences":11},"finally":{"probability":0.076923076923077,"occurences":11},"missed":{"probability":0.076923076923077,"occurences":11},"came":{"probability":0.076923076923077,"occurences":11}},"hilton":{"then":{"probability":1,"occurences":1}},"year":{"that":{"probability":1,"occurences":1}},"hyper":{"real":{"probability":0.5,"occurences":2},"produced":{"probability":0.5,"occurences":2}},"reduced":{"to":{"probability":1,"occurences":1}},"into":{"air":{"probability":0.5,"occurences":2},"a":{"probability":0.5,"occurences":2}},"inside":{"a":{"probability":0.5,"occurences":2},"video":{"probability":0.5,"occurences":2}},"paradigmatic":{"sign":{"probability":1,"occurences":1}},"stand":{"for":{"probability":1,"occurences":1}},"play":{"place":{"probability":1,"occurences":4}},"start":{"perhaps":{"probability":0.5,"occurences":2},"hurting":{"probability":0.5,"occurences":2}},"we":{"currently":{"probability":0.1,"occurences":10},"have":{"probability":0.2,"occurences":20},"are":{"probability":0.1,"occurences":10},"need":{"probability":0.1,"occurences":10},"cannot":{"probability":0.1,"occurences":10},"can":{"probability":0.1,"occurences":10},"interrupt":{"probability":0.1,"occurences":10},"will":{"probability":0.1,"occurences":10},"know":{"probability":0.1,"occurences":10}},"me":{"i":{"probability":0.047619047619048,"occurences":1},"and":{"probability":0.047619047619048,"occurences":1},"becomes":{"probability":0.047619047619048,"occurences":1},"year":{"probability":0.047619047619048,"occurences":1},"the":{"probability":0.047619047619048,"occurences":1},"back":{"probability":0.047619047619048,"occurences":1},"of":{"probability":0.095238095238095,"occurences":2},"from":{"probability":0.047619047619048,"occurences":1},"sensible":{"probability":0.047619047619048,"occurences":1},"show":{"probability":0.047619047619048,"occurences":1},"for":{"probability":0.047619047619048,"occurences":1},"here":{"probability":0.047619047619048,"occurences":1},"he's":{"probability":0.047619047619048,"occurences":1},"but":{"probability":0.047619047619048,"occurences":1},"reason":{"probability":0.047619047619048,"occurences":1},"to":{"probability":0.047619047619048,"occurences":1},"recognition":{"probability":0.047619047619048,"occurences":1},"plate":{"probability":0.047619047619048,"occurences":1},"formal":{"probability":0.047619047619048,"occurences":1},"i've":{"probability":0.047619047619048,"occurences":1}},"weaving":{"together":{"probability":1,"occurences":1}},"there's":{"no":{"probability":0.33333333333333,"occurences":3},"not":{"probability":0.33333333333333,"occurences":3},"an":{"probability":0.33333333333333,"occurences":3}},"ethereality":{"an":{"probability":1,"occurences":1}},"thing":{"new":{"probability":0.1,"occurences":2},"becomes":{"probability":0.1,"occurences":2},"however":{"probability":0.1,"occurences":2},"nostalgia":{"probability":0.1,"occurences":2},"because":{"probability":0.1,"occurences":2},"more":{"probability":0.1,"occurences":2},"we":{"probability":0.1,"occurences":2},"like":{"probability":0.1,"occurences":2},"mcdonald's":{"probability":0.2,"occurences":4}},"hype":{"real":{"probability":1,"occurences":1}},"first":{"it's":{"probability":0.5,"occurences":2},"protrusion":{"probability":0.5,"occurences":2}},"forgotten":{"where":{"probability":1,"occurences":1}},"then":{"a":{"probability":0.5,"occurences":2},"that":{"probability":0.5,"occurences":2}},"take":{"this":{"probability":1,"occurences":1}},"bad":{"but":{"probability":1,"occurences":1}},"spin-off":{"parallel":{"probability":1,"occurences":1}},"web":{"of":{"probability":1,"occurences":1}},"here's":{"the":{"probability":0.33333333333333,"occurences":4},"not":{"probability":0.16666666666667,"occurences":2},"he":{"probability":0.16666666666667,"occurences":2},"an":{"probability":0.16666666666667,"occurences":2},"no":{"probability":0.16666666666667,"occurences":2}},"referred":{"to":{"probability":1,"occurences":1}},"good":{"travis":{"probability":1,"occurences":1}},"fire":{"festival":{"probability":1,"occurences":4}},"offered":{"by":{"probability":1,"occurences":1}},"shortage":{"at":{"probability":1,"occurences":1}},"could":{"pull":{"probability":1,"occurences":1}},"entirely":{"cynical":{"probability":0.5,"occurences":2},"transcendent":{"probability":0.5,"occurences":2}},"think":{"the":{"probability":1,"occurences":1}},"traditional":{"melts":{"probability":1,"occurences":1}},"closed":{"and":{"probability":1,"occurences":1}},"dismantled":{"a":{"probability":1,"occurences":1}},"president":{"thing":{"probability":1,"occurences":1}},"make":{"a":{"probability":1,"occurences":1}},"called":{"the":{"probability":1,"occurences":4}},"bacon":{"and":{"probability":0.33333333333333,"occurences":3},"on":{"probability":0.33333333333333,"occurences":3},"now":{"probability":0.33333333333333,"occurences":3}},"out":{"much":{"probability":0.1,"occurences":1},"luxury":{"probability":0.1,"occurences":1},"really":{"probability":0.1,"occurences":1},"of":{"probability":0.1,"occurences":1},"hope":{"probability":0.1,"occurences":1},"a":{"probability":0.4,"occurences":4},"prospects":{"probability":0.1,"occurences":1}},"confluence":{"here":{"probability":1,"occurences":1}},"melt":{"the":{"probability":1,"occurences":1}},"cynical":{"and":{"probability":1,"occurences":1}},"three":{"decades":{"probability":1,"occurences":1}},"continue":{"without":{"probability":1,"occurences":1}},"ingredient":{"shortage":{"probability":1,"occurences":1}},"channel":{"then":{"probability":1,"occurences":1}},"nostalgia":{"The":{"probability":0.16666666666667,"occurences":6},"and":{"probability":0.16666666666667,"occurences":6},"repetition":{"probability":0.16666666666667,"occurences":6},"mode":{"probability":0.16666666666667,"occurences":6},"is":{"probability":0.16666666666667,"occurences":6},"look":{"probability":0.16666666666667,"occurences":6}},"more":{"you":{"probability":0.25,"occurences":2},"than":{"probability":0.25,"occurences":2},"clear":{"probability":0.25,"occurences":2},"get":{"probability":0.25,"occurences":2}},"by":{"the":{"probability":1,"occurences":1}},"personality":{"commitments":{"probability":1,"occurences":1}},"ferrari":{"sure":{"probability":1,"occurences":1}},"selling":{"me":{"probability":1,"occurences":1}},"merch":{"we":{"probability":1,"occurences":1}},"park":{"he":{"probability":1,"occurences":1}},"without":{"prospects":{"probability":0.14285714285714,"occurences":7},"a":{"probability":0.57142857142857,"occurences":28},"hope":{"probability":0.14285714285714,"occurences":7},"really":{"probability":0.14285714285714,"occurences":7}},"known":{"for":{"probability":1,"occurences":1}},"why":{"do":{"probability":1,"occurences":1}},"copulating":{"with":{"probability":1,"occurences":1}},"ambience":{"or":{"probability":1,"occurences":1}},"zodiac":{"is":{"probability":0.5,"occurences":2},"this":{"probability":0.5,"occurences":2}},"contemporaries":{"his":{"probability":1,"occurences":1}},"capacity":{"to":{"probability":1,"occurences":1}},"mode":{"a":{"probability":0.33333333333333,"occurences":3},"of":{"probability":0.66666666666667,"occurences":6}},"flying":{"over":{"probability":1,"occurences":1}},"broader":{"context":{"probability":1,"occurences":1}},"off":{"of":{"probability":0.33333333333333,"occurences":2},"a":{"probability":0.33333333333333,"occurences":2},"parallel":{"probability":0.33333333333333,"occurences":2}},"commodity":{"the":{"probability":0.33333333333333,"occurences":3},"with":{"probability":0.33333333333333,"occurences":3},"this":{"probability":0.33333333333333,"occurences":3}},"paparazzi":{"on":{"probability":1,"occurences":1}},"bird":{"i":{"probability":0.5,"occurences":2},"now":{"probability":0.5,"occurences":2}},"administration":{"of":{"probability":1,"occurences":1}},"politics":{"is":{"probability":1,"occurences":1}},"their":{"stores":{"probability":0.5,"occurences":2},"image":{"probability":0.5,"occurences":2}},"went":{"to":{"probability":1,"occurences":1}},"would":{"continue":{"probability":1,"occurences":1}},"culture":{"would":{"probability":1,"occurences":1}},"situation":{"in":{"probability":1,"occurences":1}},"mark":{"the":{"probability":0.5,"occurences":2},"fisher":{"probability":0.5,"occurences":2}},"perfectly":{"self-referential":{"probability":0.5,"occurences":2},"malleable":{"probability":0.5,"occurences":2}},"simultaneity":{"of":{"probability":1,"occurences":1}},"not":{"his":{"probability":0.076923076923077,"occurences":12},"known":{"probability":0.076923076923077,"occurences":12},"much":{"probability":0.076923076923077,"occurences":12},"all":{"probability":0.076923076923077,"occurences":12},"generated":{"probability":0.076923076923077,"occurences":12},"every":{"probability":0.076923076923077,"occurences":12},"try":{"probability":0.076923076923077,"occurences":12},"exactly":{"probability":0.15384615384615,"occurences":24},"melted":{"probability":0.076923076923077,"occurences":12},"children":{"probability":0.076923076923077,"occurences":12},"a":{"probability":0.076923076923077,"occurences":12},"those":{"probability":0.076923076923077,"occurences":12}},"source":{"of":{"probability":1,"occurences":1}},"remind":{"us":{"probability":1,"occurences":1}},"coincidentally":{"without":{"probability":0.5,"occurences":2},"there":{"probability":0.5,"occurences":2}},"happiest":{"days":{"probability":1,"occurences":1}},"our":{"happiest":{"probability":0.25,"occurences":1},"brain's":{"probability":0.25,"occurences":1},"fans":{"probability":0.25,"occurences":1},"head":{"probability":0.25,"occurences":1}},"ostensibly":{"our":{"probability":1,"occurences":1}},"you":{"know":{"probability":0.125,"occurences":8},"can":{"probability":0.25,"occurences":16},"want":{"probability":0.125,"occurences":8},"heard":{"probability":0.125,"occurences":8},"can't":{"probability":0.125,"occurences":8},"do":{"probability":0.125,"occurences":8},"won't":{"probability":0.125,"occurences":8}},"us":{"the":{"probability":0.125,"occurences":3},"when":{"probability":0.125,"occurences":3},"and":{"probability":0.125,"occurences":3},"The":{"probability":0.125,"occurences":3},"gonna":{"probability":0.125,"occurences":3},"is":{"probability":0.125,"occurences":3},"manner":{"probability":0.125,"occurences":3},"what's":{"probability":0.125,"occurences":3}},"gonna":{"go":{"probability":1,"occurences":4}},"sure":{"but":{"probability":1,"occurences":1}},"got":{"a":{"probability":1,"occurences":1}},"sadness":{"he's":{"probability":1,"occurences":1}},"suddenly":{"this":{"probability":1,"occurences":1}},"know":{"stand":{"probability":0.5,"occurences":2},"mcdonald's":{"probability":0.5,"occurences":2}},"news":{"bullet":{"probability":1,"occurences":1}},"when":{"i":{"probability":0.25,"occurences":4},"scott":{"probability":0.25,"occurences":4},"ostensibly":{"probability":0.25,"occurences":4},"images":{"probability":0.25,"occurences":4}},"return":{"to":{"probability":1,"occurences":1}},"workers":{"not":{"probability":1,"occurences":1}},"album":{"astroworld":{"probability":1,"occurences":1}},"amusement":{"park":{"probability":1,"occurences":1}},"wait":{"to":{"probability":1,"occurences":1}},"brain's":{"not":{"probability":1,"occurences":1}},"except":{"that":{"probability":1,"occurences":1}},"theme":{"of":{"probability":1,"occurences":1}},"come":{"the":{"probability":0.5,"occurences":1},"from":{"probability":0.5,"occurences":1}},"politicize":{"their":{"probability":1,"occurences":1}},"[":{"__":{"probability":1,"occurences":1}},"bleak":{"this":{"probability":1,"occurences":1}},"astroworld":{"named":{"probability":1,"occurences":1}},"repetition":{"for":{"probability":1,"occurences":1}},"unique":{"and":{"probability":1,"occurences":1}},"flew":{"around":{"probability":1,"occurences":1}},"witnessing":{"a":{"probability":1,"occurences":1}},"flat":{"nostalgia":{"probability":1,"occurences":1}},"won't":{"have":{"probability":1,"occurences":1}},"can't":{"go":{"probability":1,"occurences":1}},"go":{"to":{"probability":0.25,"occurences":4},"when":{"probability":0.25,"occurences":4},"where":{"probability":0.25,"occurences":4},"get":{"probability":0.25,"occurences":4}},"own":{"for":{"probability":0.33333333333333,"occurences":1},"anything":{"probability":0.33333333333333,"occurences":1},"that":{"probability":0.33333333333333,"occurences":1}},"hope":{"without":{"probability":1,"occurences":1}},"between":{"without":{"probability":1,"occurences":1}},"saw":{"The":{"probability":1,"occurences":1}},"or":{"this":{"probability":0.095238095238095,"occurences":10},"his":{"probability":0.047619047619048,"occurences":5},"something":{"probability":0.047619047619048,"occurences":5},"free":{"probability":0.047619047619048,"occurences":5},"sale":{"probability":0.047619047619048,"occurences":5},"another":{"probability":0.047619047619048,"occurences":5},"message":{"probability":0.047619047619048,"occurences":5},"which":{"probability":0.047619047619048,"occurences":5},"no":{"probability":0.047619047619048,"occurences":5},"copulating":{"probability":0.047619047619048,"occurences":5},"doing":{"probability":0.047619047619048,"occurences":5},"what":{"probability":0.047619047619048,"occurences":5},"depthlessness":{"probability":0.047619047619048,"occurences":5},"the":{"probability":0.095238095238095,"occurences":10},"a":{"probability":0.19047619047619,"occurences":20},"those":{"probability":0.047619047619048,"occurences":5}},"reason":{"it's":{"probability":0.5,"occurences":2},"except":{"probability":0.5,"occurences":2}},"children":{"for":{"probability":1,"occurences":1}},"never":{"heard":{"probability":1,"occurences":1}},"amorphous":{"and":{"probability":0.5,"occurences":2},"is":{"probability":0.5,"occurences":2}},"above":{"most":{"probability":1,"occurences":1}},"cannot":{"try":{"probability":1,"occurences":1}},"it's":{"almost":{"probability":0.11111111111111,"occurences":9},"the":{"probability":0.11111111111111,"occurences":9},"amazing":{"probability":0.11111111111111,"occurences":9},"not":{"probability":0.11111111111111,"occurences":9},"ubiquitous":{"probability":0.11111111111111,"occurences":9},"unhealthy":{"probability":0.11111111111111,"occurences":9},"a":{"probability":0.11111111111111,"occurences":9},"being":{"probability":0.11111111111111,"occurences":9},"pretty":{"probability":0.11111111111111,"occurences":9}},"anger":{"politicize":{"probability":1,"occurences":1}},"40":{"and":{"probability":1,"occurences":1}},"pounder":{"with":{"probability":1,"occurences":1}},"with":{"drake":{"probability":0.083333333333333,"occurences":12},"something":{"probability":0.083333333333333,"occurences":12},"all":{"probability":0.083333333333333,"occurences":12},"the":{"probability":0.083333333333333,"occurences":12},"trump":{"probability":0.083333333333333,"occurences":12},"bacon":{"probability":0.25,"occurences":36},"sadness":{"probability":0.083333333333333,"occurences":12},"positive":{"probability":0.083333333333333,"occurences":12},"a":{"probability":0.083333333333333,"occurences":12},"any":{"probability":0.083333333333333,"occurences":12}},"rap":{"artist":{"probability":1,"occurences":1}},"target":{"of":{"probability":1,"occurences":1}},"parties":{"in":{"probability":1,"occurences":1}},"partial":{"explanation":{"probability":1,"occurences":1}},"have":{"the":{"probability":0.125,"occurences":8},"been":{"probability":0.125,"occurences":8},"to":{"probability":0.125,"occurences":8},"what":{"probability":0.125,"occurences":8},"fun":{"probability":0.125,"occurences":8},"for":{"probability":0.125,"occurences":8},"a":{"probability":0.125,"occurences":8},"never":{"probability":0.125,"occurences":8}},"solid":{"traditional":{"probability":1,"occurences":1}},"sign":{"of":{"probability":1,"occurences":1}},"density":{"this":{"probability":1,"occurences":1}},"__":{"]":{"probability":1,"occurences":1}},"what":{"bodriar":{"probability":0.125,"occurences":8},"past":{"probability":0.125,"occurences":8},"does":{"probability":0.125,"occurences":8},"else":{"probability":0.125,"occurences":8},"we":{"probability":0.125,"occurences":8},"frederick":{"probability":0.25,"occurences":16},"sets":{"probability":0.125,"occurences":8}},"popular":{"for":{"probability":1,"occurences":1}},"fisher":{"referred":{"probability":1,"occurences":1}},"system":[],"stray":{"much":{"probability":1,"occurences":1}},"functions":{"without":{"probability":1,"occurences":1}},"many":{"future":{"probability":1,"occurences":1}},"produced":{"but":{"probability":1,"occurences":1}},"align":{"he's":{"probability":1,"occurences":1}},"said":{"all":{"probability":1,"occurences":1}},"seems":{"to":{"probability":1,"occurences":4}},"criticism":{"i":{"probability":1,"occurences":1}},"was":{"born":{"probability":0.25,"occurences":16},"introduced":{"probability":0.125,"occurences":8},"of":{"probability":0.125,"occurences":8},"michael":{"probability":0.125,"occurences":8},"the":{"probability":0.25,"occurences":16},"lost":{"probability":0.125,"occurences":8}},"see":{"is":{"probability":0.5,"occurences":2},"how":{"probability":0.5,"occurences":2}},"head":{"and":{"probability":1,"occurences":1}},"will":{"have":{"probability":1,"occurences":1}},"didn't":{"have":{"probability":1,"occurences":1}},"indifferent":{"perfectly":{"probability":1,"occurences":1}},"few":{"have":{"probability":1,"occurences":1}},"ambiguity":{"that":{"probability":1,"occurences":1}},"over":{"the":{"probability":0.66666666666667,"occurences":6},"which":{"probability":0.33333333333333,"occurences":3}},"disappearance":{"of":{"probability":1,"occurences":1}},"some":{"sensible":{"probability":0.5,"occurences":2},"of":{"probability":0.5,"occurences":2}},"predatory":{"and":{"probability":1,"occurences":1}},"yeah":{"you":{"probability":1,"occurences":1}},"lyrically":{"lyrically":{"probability":1,"occurences":2}},"world":{"radically":{"probability":0.33333333333333,"occurences":2},"named":{"probability":0.33333333333333,"occurences":2},"two":{"probability":0.33333333333333,"occurences":2}},"around":{"on":{"probability":1,"occurences":1}},"requirement":{"to":{"probability":1,"occurences":1}},"means":{"the":{"probability":0.66666666666667,"occurences":6},"there":{"probability":0.33333333333333,"occurences":3}},"invest":{"it":{"probability":1,"occurences":1}},"mystique":{"an":{"probability":1,"occurences":1}},"mass":{"doesn't":{"probability":1,"occurences":1}},"been":{"the":{"probability":0.33333333333333,"occurences":3},"pretty":{"probability":0.33333333333333,"occurences":3},"lost":{"probability":0.33333333333333,"occurences":3}},"childhood":{"i'm":{"probability":0.5,"occurences":2},"via":{"probability":0.5,"occurences":2}},"scott":{"and":{"probability":0.125,"occurences":32},"was":{"probability":0.125,"occurences":32},"image":{"probability":0.125,"occurences":32},"is":{"probability":0.0625,"occurences":16},"why":{"probability":0.0625,"occurences":16},"surrealist":{"probability":0.0625,"occurences":16},"burger":{"probability":0.0625,"occurences":16},"can":{"probability":0.0625,"occurences":16},"berger":{"probability":0.0625,"occurences":16},"in":{"probability":0.0625,"occurences":16},"to":{"probability":0.0625,"occurences":16},"as":{"probability":0.0625,"occurences":16},"politically":{"probability":0.0625,"occurences":16}},"exactly":{"a":{"probability":1,"occurences":4}},"there":{"seems":{"probability":0.16666666666667,"occurences":6},"is":{"probability":0.5,"occurences":18},"to":{"probability":0.16666666666667,"occurences":6},"has":{"probability":0.16666666666667,"occurences":6}},"unhealthy":{"to":{"probability":1,"occurences":1}},"sets":{"travis":{"probability":1,"occurences":1}},"level":{"but":{"probability":0.33333333333333,"occurences":3},"of":{"probability":0.33333333333333,"occurences":3},"so":{"probability":0.33333333333333,"occurences":3}},"for":{"his":{"probability":0.0625,"occurences":16},"something":{"probability":0.0625,"occurences":16},"free":{"probability":0.0625,"occurences":16},"sale":{"probability":0.0625,"occurences":16},"the":{"probability":0.125,"occurences":32},"copulating":{"probability":0.0625,"occurences":16},"doing":{"probability":0.0625,"occurences":16},"this":{"probability":0.125,"occurences":32},"which":{"probability":0.0625,"occurences":16},"no":{"probability":0.0625,"occurences":16},"a":{"probability":0.1875,"occurences":48},"those":{"probability":0.0625,"occurences":16}},"concert":{"inside":{"probability":1,"occurences":1}},"born":{"coincidentally":{"probability":0.5,"occurences":2},"this":{"probability":0.5,"occurences":2}},"videos":{"in":{"probability":1,"occurences":1}},"another":{"is":{"probability":1,"occurences":1}},"become":{"the":{"probability":1,"occurences":1}},"post-modernity":{"we":{"probability":1,"occurences":1}},"place":{"travis":{"probability":0.33333333333333,"occurences":3},"anymore":{"probability":0.33333333333333,"occurences":3},"happy":{"probability":0.33333333333333,"occurences":3}},"marx":{"said":{"probability":1,"occurences":1}},"universally":{"liquidates":{"probability":1,"occurences":1}},"jameson":{"refers":{"probability":0.33333333333333,"occurences":3},"called":{"probability":0.33333333333333,"occurences":3},"claims":{"probability":0.33333333333333,"occurences":3}},"live":{"it":{"probability":1,"occurences":1}},"perfect":{"positive":{"probability":0.33333333333333,"occurences":3},"surface":{"probability":0.33333333333333,"occurences":3},"because":{"probability":0.33333333333333,"occurences":3}},"end":{"of":{"probability":0.33333333333333,"occurences":1},"at":{"probability":0.33333333333333,"occurences":1},"themselves":{"probability":0.33333333333333,"occurences":1}},"get":{"of":{"probability":0.25,"occurences":3},"your":{"probability":0.25,"occurences":3},"lit":{"probability":0.5,"occurences":6}},"are":{"any":{"probability":0.16666666666667,"occurences":6},"not":{"probability":0.16666666666667,"occurences":6},"witnessing":{"probability":0.16666666666667,"occurences":6},"connected":{"probability":0.16666666666667,"occurences":6},"saturated":{"probability":0.16666666666667,"occurences":6},"behind":{"probability":0.16666666666667,"occurences":6}},"interference":{"of":{"probability":1,"occurences":1}},"and":{"i":{"probability":0.0625,"occurences":62},"who":{"probability":0.03125,"occurences":31},"where":{"probability":0.03125,"occurences":31},"the":{"probability":0.0625,"occurences":62},"not":{"probability":0.03125,"occurences":31},"can":{"probability":0.03125,"occurences":31},"doesn't":{"probability":0.03125,"occurences":31},"of":{"probability":0.03125,"occurences":31},"lettuce":{"probability":0.03125,"occurences":31},"being":{"probability":0.03125,"occurences":31},"this":{"probability":0.0625,"occurences":62},"dismantled":{"probability":0.03125,"occurences":31},"for":{"probability":0.03125,"occurences":31},"these":{"probability":0.03125,"occurences":31},"before":{"probability":0.03125,"occurences":31},"it's":{"probability":0.09375,"occurences":93},"they're":{"probability":0.03125,"occurences":31},"if":{"probability":0.03125,"occurences":31},"amorphous":{"probability":0.03125,"occurences":31},"travis":{"probability":0.03125,"occurences":31},"he":{"probability":0.03125,"occurences":31},"rappers":{"probability":0.03125,"occurences":31},"anger":{"probability":0.03125,"occurences":31},"mcdonald's":{"probability":0.03125,"occurences":31},"above":{"probability":0.03125,"occurences":31},"coincidentally":{"probability":0.03125,"occurences":31},"mark":{"probability":0.03125,"occurences":31}},"can":{"make":{"probability":0.125,"occurences":8},"overcode":{"probability":0.125,"occurences":8},"align":{"probability":0.125,"occurences":8},"appropriate":{"probability":0.125,"occurences":8},"bet":{"probability":0.125,"occurences":8},"see":{"probability":0.125,"occurences":8},"already":{"probability":0.125,"occurences":8},"invest":{"probability":0.125,"occurences":8}},"now":{"bearing":{"probability":0.1,"occurences":8},"don't":{"probability":0.1,"occurences":8},"insofar":{"probability":0.1,"occurences":8},"look":{"probability":0.1,"occurences":8},"this":{"probability":0.1,"occurences":8},"looks":{"probability":0.1,"occurences":8},"stand":{"probability":0.1,"occurences":8},"and":{"probability":0.1,"occurences":8},"what":{"probability":0.1,"occurences":8},"mcdonald's":{"probability":0.1,"occurences":8}},"history":{"here's":{"probability":1,"occurences":1}},"commodities":{"drugs":{"probability":1,"occurences":1}},"free":{"this":{"probability":1,"occurences":1}},"capitalism":{"and":{"probability":0.33333333333333,"occurences":3},"one":{"probability":0.33333333333333,"occurences":3},"mcdonald's":{"probability":0.33333333333333,"occurences":3}},"transition":{"to":{"probability":1,"occurences":1}},"sense":{"The":{"probability":1,"occurences":1}},"matter":{"the":{"probability":1,"occurences":1}},"expectation":{"that":{"probability":1,"occurences":1}},"time":{"i've":{"probability":1,"occurences":1}},"peak":{"hyper":{"probability":1,"occurences":1}},"boomers":{"let's":{"probability":1,"occurences":1}},"roughly":{"his":{"probability":1,"occurences":1}},"remain":{"under":{"probability":1,"occurences":1}},"he":{"went":{"probability":0.011111111111111,"occurences":6},"simultaneity":{"probability":0.011111111111111,"occurences":6},"aforementioned":{"probability":0.011111111111111,"occurences":6},"future":{"probability":0.044444444444444,"occurences":24},"first":{"probability":0.011111111111111,"occurences":6},"simulation":{"probability":0.011111111111111,"occurences":6},"source":{"probability":0.011111111111111,"occurences":6},"mcdonald's":{"probability":0.022222222222222,"occurences":12},"spin-off":{"probability":0.011111111111111,"occurences":6},"game":{"probability":0.011111111111111,"occurences":6},"hyperreal":{"probability":0.011111111111111,"occurences":6},"event":{"probability":0.011111111111111,"occurences":6},"acceptance":{"probability":0.011111111111111,"occurences":6},"purpose":{"probability":0.011111111111111,"occurences":6},"last":{"probability":0.033333333333333,"occurences":18},"flew":{"probability":0.011111111111111,"occurences":6},"happy":{"probability":0.011111111111111,"occurences":6},"doesn't":{"probability":0.022222222222222,"occurences":12},"well-trodden":{"probability":0.011111111111111,"occurences":6},"generation":{"probability":0.011111111111111,"occurences":6},"is":{"probability":0.011111111111111,"occurences":6},"current":{"probability":0.011111111111111,"occurences":6},"post-modernisms":{"probability":0.011111111111111,"occurences":6},"boomers":{"probability":0.011111111111111,"occurences":6},"peak":{"probability":0.011111111111111,"occurences":6},"same":{"probability":0.022222222222222,"occurences":12},"commodity":{"probability":0.011111111111111,"occurences":6},"largest":{"probability":0.011111111111111,"occurences":6},"supreme":{"probability":0.011111111111111,"occurences":6},"material":{"probability":0.011111111111111,"occurences":6},"nostalgia":{"probability":0.011111111111111,"occurences":6},"theme":{"probability":0.011111111111111,"occurences":6},"whole":{"probability":0.022222222222222,"occurences":12},"realist":{"probability":0.011111111111111,"occurences":6},"threads":{"probability":0.011111111111111,"occurences":6},"travis":{"probability":0.044444444444444,"occurences":24},"most":{"probability":0.011111111111111,"occurences":6},"disappearance":{"probability":0.011111111111111,"occurences":6},"25":{"probability":0.011111111111111,"occurences":6},"merch":{"probability":0.011111111111111,"occurences":6},"sententious":{"probability":0.011111111111111,"occurences":6},"paparazzi":{"probability":0.011111111111111,"occurences":6},"goal":{"probability":0.011111111111111,"occurences":6},"deterioration":{"probability":0.011111111111111,"occurences":6},"surrealist":{"probability":0.022222222222222,"occurences":12},"sistine":{"probability":0.011111111111111,"occurences":6},"flame":{"probability":0.011111111111111,"occurences":6},"sound":{"probability":0.011111111111111,"occurences":6},"name":{"probability":0.011111111111111,"occurences":6},"target":{"probability":0.011111111111111,"occurences":6},"entire":{"probability":0.011111111111111,"occurences":6},"paradigmatic":{"probability":0.011111111111111,"occurences":6},"appearance":{"probability":0.011111111111111,"occurences":6},"world":{"probability":0.011111111111111,"occurences":6},"transition":{"probability":0.011111111111111,"occurences":6},"image":{"probability":0.044444444444444,"occurences":24},"level":{"probability":0.011111111111111,"occurences":6},"burger":{"probability":0.022222222222222,"occurences":12},"one":{"probability":0.011111111111111,"occurences":6},"gonna":{"probability":0.011111111111111,"occurences":6},"play":{"probability":0.011111111111111,"occurences":6},"capacity":{"probability":0.011111111111111,"occurences":6},"mode":{"probability":0.011111111111111,"occurences":6},"crowd":{"probability":0.011111111111111,"occurences":6},"golden":{"probability":0.011111111111111,"occurences":6},"broader":{"probability":0.011111111111111,"occurences":6},"videos":{"probability":0.011111111111111,"occurences":6},"past":{"probability":0.022222222222222,"occurences":12},"nihilism":{"probability":0.011111111111111,"occurences":6},"age":{"probability":0.011111111111111,"occurences":6},"end":{"probability":0.011111111111111,"occurences":6},"administration":{"probability":0.011111111111111,"occurences":6}},"malleable":{"cool":{"probability":1,"occurences":1}},"past":{"now":{"probability":0.16666666666667,"occurences":6},"becomes":{"probability":0.16666666666667,"occurences":6},"three":{"probability":0.16666666666667,"occurences":6},"is":{"probability":0.16666666666667,"occurences":6},"without":{"probability":0.33333333333333,"occurences":12}},"luxury":{"good":{"probability":0.5,"occurences":2},"commodities":{"probability":0.5,"occurences":2}},"image":{"this":{"probability":0.076923076923077,"occurences":13},"is":{"probability":0.15384615384615,"occurences":26},"unlike":{"probability":0.076923076923077,"occurences":13},"but":{"probability":0.076923076923077,"occurences":13},"not":{"probability":0.076923076923077,"occurences":13},"apart":{"probability":0.076923076923077,"occurences":13},"the":{"probability":0.076923076923077,"occurences":13},"of":{"probability":0.15384615384615,"occurences":26},"though":{"probability":0.076923076923077,"occurences":13},"being":{"probability":0.076923076923077,"occurences":13},"most":{"probability":0.076923076923077,"occurences":13}},"festival":{"and":{"probability":1,"occurences":4}},"25":{"this":{"probability":1,"occurences":1}},"how":{"president":{"probability":0.5,"occurences":1},"many":{"probability":0.5,"occurences":1}},"i've":{"been":{"probability":1,"occurences":1}},"before":{"the":{"probability":1,"occurences":1}},"refers":{"to":{"probability":1,"occurences":1}},"already":{"established":{"probability":0.5,"occurences":2},"add":{"probability":0.5,"occurences":2}},"sistine":{"chapel":{"probability":1,"occurences":1}},"name":{"but":{"probability":0.25,"occurences":4},"plate":{"probability":0.25,"occurences":4},"he's":{"probability":0.25,"occurences":4},"recognition":{"probability":0.25,"occurences":4}},"together":{"the":{"probability":1,"occurences":1}},"fans":{"getting":{"probability":0.5,"occurences":2},"lit":{"probability":0.5,"occurences":2}},"thousands":{"of":{"probability":1,"occurences":1}},"video":{"to":{"probability":0.25,"occurences":4},"with":{"probability":0.25,"occurences":4},"game":{"probability":0.5,"occurences":8}},"so":{"what":{"probability":0.33333333333333,"occurences":3},"much":{"probability":0.33333333333333,"occurences":3},"let's":{"probability":0.33333333333333,"occurences":3}},"serve":{"to":{"probability":1,"occurences":1}},"heard":{"of":{"probability":0.5,"occurences":2},"that":{"probability":0.5,"occurences":2}},"look":{"back":{"probability":0.5,"occurences":2},"here":{"probability":0.5,"occurences":2}},"show":{"president":{"probability":1,"occurences":1}},"model":{"with":{"probability":0.5,"occurences":2},"management":{"probability":0.5,"occurences":2}},"looks":{"something":{"probability":1,"occurences":1}},"themselves":{"to":{"probability":0.5,"occurences":2},"we":{"probability":0.5,"occurences":2}},"long":{"time":{"probability":1,"occurences":1}},"media":{"forms":{"probability":1,"occurences":1}},"forms":{"from":{"probability":1,"occurences":1}},"different":{"perfectly":{"probability":0.5,"occurences":1},"from":{"probability":0.5,"occurences":1}},"guys":{"i":{"probability":1,"occurences":1}},"simulation":{"of":{"probability":1,"occurences":1}},"generation":{"in":{"probability":1,"occurences":1}},"capitalist":{"system":{"probability":1,"occurences":1}},"collaboration":{"is":{"probability":1,"occurences":1}},"sale":{"here":{"probability":1,"occurences":1}},"message":{"he":{"probability":1,"occurences":1}},"berger":{"and":{"probability":1,"occurences":1}},"outgrowth":{"but":{"probability":1,"occurences":1}},"signification":{"there":{"probability":1,"occurences":1}},"bet":{"if":{"probability":1,"occurences":1}},"should":{"remain":{"probability":1,"occurences":1}},"sound":{"is":{"probability":1,"occurences":1}},"entire":{"hyperreal":{"probability":1,"occurences":1}},"outside":{"of":{"probability":1,"occurences":1}},"named":{"after":{"probability":1,"occurences":1}},"giant":{"mechanical":{"probability":1,"occurences":4}},"self-referential":{"universe":{"probability":1,"occurences":1}},"worked":{"itself":{"probability":1,"occurences":1}},"women":{"and":{"probability":1,"occurences":1}},"cliff":{"off":{"probability":1,"occurences":1}},"that's":{"it":{"probability":1,"occurences":1}},"threads":{"of":{"probability":1,"occurences":1}},"bearing":{"the":{"probability":1,"occurences":1}},"light":{"the":{"probability":1,"occurences":1}},"realize":{"something":{"probability":1,"occurences":1}},"global":{"capitalism":{"probability":1,"occurences":1}},"unlike":{"some":{"probability":1,"occurences":1}},"protrusion":{"of":{"probability":1,"occurences":1}},"rather":{"he's":{"probability":1,"occurences":1}},"this":{"i":{"probability":0.028571428571429,"occurences":35},"offered":{"probability":0.028571428571429,"occurences":35},"and":{"probability":0.028571428571429,"occurences":35},"guy":{"probability":0.028571428571429,"occurences":35},"so":{"probability":0.028571428571429,"occurences":35},"photograph":{"probability":0.028571428571429,"occurences":35},"nostalgia":{"probability":0.028571428571429,"occurences":35},"outgrowth":{"probability":0.028571428571429,"occurences":35},"black":{"probability":0.028571428571429,"occurences":35},"whole":{"probability":0.028571428571429,"occurences":35},"was":{"probability":0.028571428571429,"occurences":35},"program":{"probability":0.028571428571429,"occurences":35},"is":{"probability":0.34285714285714,"occurences":420},"universal":{"probability":0.028571428571429,"occurences":35},"height":{"probability":0.028571428571429,"occurences":35},"only":{"probability":0.028571428571429,"occurences":35},"particular":{"probability":0.028571428571429,"occurences":35},"image":{"probability":0.057142857142857,"occurences":70},"as":{"probability":0.057142857142857,"occurences":70},"lends":{"probability":0.028571428571429,"occurences":35},"video":{"probability":0.028571428571429,"occurences":35},"rap":{"probability":0.028571428571429,"occurences":35}},"causing":{"mcdonald's":{"probability":1,"occurences":1}},"yarns":{"about":{"probability":1,"occurences":1}},"has":{"to":{"probability":0.5,"occurences":2},"worked":{"probability":0.5,"occurences":2}},"down":{"that":{"probability":1,"occurences":1}},"largest":{"restaurant":{"probability":1,"occurences":1}},"remember":{"birthday":{"probability":1,"occurences":1}},"program":{"for":{"probability":1,"occurences":1}},"liquidates":{"locality":{"probability":1,"occurences":1}},"intoxicating":{"litmus":{"probability":1,"occurences":1}},"travis":{"scott":{"probability":0.88235294117647,"occurences":255},"scott's":{"probability":0.11764705882353,"occurences":34}},"exploits":{"hundreds":{"probability":1,"occurences":1}},"product":{"we":{"probability":1,"occurences":1}},"music":{"The":{"probability":1,"occurences":1}},"ambient":{"mass":{"probability":1,"occurences":1}},"game":{"show":{"probability":0.33333333333333,"occurences":3},"for":{"probability":0.33333333333333,"occurences":3},"i":{"probability":0.33333333333333,"occurences":3}},"event":{"in":{"probability":0.33333333333333,"occurences":3},"after":{"probability":0.33333333333333,"occurences":3},"that":{"probability":0.33333333333333,"occurences":3}},"getting":{"lit":{"probability":1,"occurences":1}},"after":{"the":{"probability":0.5,"occurences":2},"an":{"probability":0.5,"occurences":2}},"at":{"i":{"probability":0.037037037037037,"occurences":4},"does":{"probability":0.037037037037037,"occurences":4},"else":{"probability":0.037037037037037,"occurences":4},"density":{"probability":0.037037037037037,"occurences":4},"the":{"probability":0.074074074074074,"occurences":8},"past":{"probability":0.037037037037037,"occurences":4},"nostalgia":{"probability":0.037037037037037,"occurences":4},"frederick":{"probability":0.074074074074074,"occurences":8},"you":{"probability":0.037037037037037,"occurences":4},"mcdonald's":{"probability":0.037037037037037,"occurences":4},"we":{"probability":0.074074074074074,"occurences":8},"right":{"probability":0.037037037037037,"occurences":4},"was":{"probability":0.037037037037037,"occurences":4},"however":{"probability":0.037037037037037,"occurences":4},"is":{"probability":0.037037037037037,"occurences":4},"it":{"probability":0.037037037037037,"occurences":4},"travis":{"probability":0.037037037037037,"occurences":4},"enables":{"probability":0.037037037037037,"occurences":4},"it's":{"probability":0.037037037037037,"occurences":4},"every":{"probability":0.037037037037037,"occurences":4},"bodriar":{"probability":0.037037037037037,"occurences":4},"sets":{"probability":0.037037037037037,"occurences":4},"left":{"probability":0.037037037037037,"occurences":4},"least":{"probability":0.037037037037037,"occurences":4}},"doubles":{"as":{"probability":1,"occurences":1}},"imagery":{"now":{"probability":1,"occurences":1}},"stalwart":{"is":{"probability":1,"occurences":1}},"while":{"he":{"probability":0.5,"occurences":2},"there":{"probability":0.5,"occurences":2}},"flame":{"here":{"probability":1,"occurences":1}},"temple":{"of":{"probability":1,"occurences":1}},"you've":{"forgotten":{"probability":1,"occurences":1}},"under":{"observation":{"probability":0.5,"occurences":1},"with":{"probability":0.5,"occurences":1}},"no":{"requirement":{"probability":0.5,"occurences":2},"reason":{"probability":0.5,"occurences":2}},"drops":{"inside":{"probability":1,"occurences":1}},"add":{"lettuce":{"probability":0.5,"occurences":2},"up":{"probability":0.5,"occurences":2}},"drugs":{"women":{"probability":1,"occurences":1}},"chain":{"in":{"probability":1,"occurences":1}},"melts":{"into":{"probability":1,"occurences":1}},"claims":{"that":{"probability":1,"occurences":1}},"the":{"same":{"probability":0.025316455696203,"occurences":158},"commodity":{"probability":0.012658227848101,"occurences":79},"largest":{"probability":0.012658227848101,"occurences":79},"peak":{"probability":0.012658227848101,"occurences":79},"aforementioned":{"probability":0.012658227848101,"occurences":79},"future":{"probability":0.050632911392405,"occurences":316},"first":{"probability":0.012658227848101,"occurences":79},"end":{"probability":0.012658227848101,"occurences":79},"material":{"probability":0.012658227848101,"occurences":79},"nostalgia":{"probability":0.012658227848101,"occurences":79},"boomers":{"probability":0.012658227848101,"occurences":79},"golden":{"probability":0.012658227848101,"occurences":79},"nihilism":{"probability":0.012658227848101,"occurences":79},"simulation":{"probability":0.012658227848101,"occurences":79},"mcdonald's":{"probability":0.025316455696203,"occurences":158},"sistine":{"probability":0.012658227848101,"occurences":79},"disappearance":{"probability":0.012658227848101,"occurences":79},"spin-off":{"probability":0.012658227848101,"occurences":79},"travis":{"probability":0.050632911392405,"occurences":316},"realist":{"probability":0.012658227848101,"occurences":79},"threads":{"probability":0.012658227848101,"occurences":79},"hyperreal":{"probability":0.012658227848101,"occurences":79},"game":{"probability":0.012658227848101,"occurences":79},"25":{"probability":0.012658227848101,"occurences":79},"most":{"probability":0.012658227848101,"occurences":79},"surrealist":{"probability":0.025316455696203,"occurences":158},"event":{"probability":0.012658227848101,"occurences":79},"acceptance":{"probability":0.012658227848101,"occurences":79},"age":{"probability":0.012658227848101,"occurences":79},"burger":{"probability":0.025316455696203,"occurences":158},"simultaneity":{"probability":0.012658227848101,"occurences":79},"generation":{"probability":0.012658227848101,"occurences":79},"paparazzi":{"probability":0.012658227848101,"occurences":79},"well-trodden":{"probability":0.012658227848101,"occurences":79},"deterioration":{"probability":0.012658227848101,"occurences":79},"source":{"probability":0.012658227848101,"occurences":79},"purpose":{"probability":0.012658227848101,"occurences":79},"flame":{"probability":0.012658227848101,"occurences":79},"transition":{"probability":0.012658227848101,"occurences":79},"world":{"probability":0.012658227848101,"occurences":79},"sententious":{"probability":0.012658227848101,"occurences":79},"entire":{"probability":0.012658227848101,"occurences":79},"happy":{"probability":0.012658227848101,"occurences":79},"appearance":{"probability":0.012658227848101,"occurences":79},"play":{"probability":0.012658227848101,"occurences":79},"paradigmatic":{"probability":0.012658227848101,"occurences":79},"image":{"probability":0.050632911392405,"occurences":316},"level":{"probability":0.012658227848101,"occurences":79},"past":{"probability":0.012658227848101,"occurences":79},"one":{"probability":0.012658227848101,"occurences":79},"videos":{"probability":0.012658227848101,"occurences":79},"last":{"probability":0.037974683544304,"occurences":237},"capacity":{"probability":0.012658227848101,"occurences":79},"mode":{"probability":0.012658227848101,"occurences":79},"crowd":{"probability":0.012658227848101,"occurences":79},"whole":{"probability":0.025316455696203,"occurences":158},"broader":{"probability":0.012658227848101,"occurences":79},"theme":{"probability":0.012658227848101,"occurences":79},"current":{"probability":0.012658227848101,"occurences":79},"post-modernisms":{"probability":0.012658227848101,"occurences":79},"target":{"probability":0.012658227848101,"occurences":79},"name":{"probability":0.012658227848101,"occurences":79},"administration":{"probability":0.012658227848101,"occurences":79}},"you're":{"over":{"probability":1,"occurences":1}},"commitments":{"or":{"probability":1,"occurences":1}},"way":{"back":{"probability":0.5,"occurences":1},"and":{"probability":0.5,"occurences":1}},"formal":{"feature":{"probability":1,"occurences":1}},"burger":{"for":{"probability":0.125,"occurences":8},"few":{"probability":0.125,"occurences":8},"here's":{"probability":0.125,"occurences":8},"with":{"probability":0.25,"occurences":16},"meal":{"probability":0.125,"occurences":8},"is":{"probability":0.125,"occurences":8},"doesn't":{"probability":0.125,"occurences":8}},"going":{"to":{"probability":1,"occurences":1}},"saturated":{"with":{"probability":1,"occurences":1}},"]":{"grating":{"probability":1,"occurences":1}},"orbit":{"of":{"probability":1,"occurences":1}},"kind":{"of":{"probability":1,"occurences":9}},"which":{"culture":{"probability":0.1,"occurences":10},"was":{"probability":0.1,"occurences":10},"travis":{"probability":0.1,"occurences":10},"capital":{"probability":0.1,"occurences":10},"he":{"probability":0.1,"occurences":10},"now":{"probability":0.1,"occurences":10},"celebrities":{"probability":0.1,"occurences":10},"doubles":{"probability":0.1,"occurences":10},"of":{"probability":0.1,"occurences":10},"we":{"probability":0.1,"occurences":10}},"lends":{"to":{"probability":1,"occurences":1}},"finally":{"recognized":{"probability":1,"occurences":1}},"most":{"literal":{"probability":0.2,"occurences":4},"stalwart":{"probability":0.2,"occurences":4},"a":{"probability":0.2,"occurences":4},"popular":{"probability":0.2,"occurences":4},"of":{"probability":0.2,"occurences":4}}} \ No newline at end of file diff --git a/todo-stuff/enforcer/init.lua b/todo-stuff/enforcer/init.lua new file mode 100644 index 0000000..7099494 --- /dev/null +++ b/todo-stuff/enforcer/init.lua @@ -0,0 +1,478 @@ +--TODO: Add domain-specific manuals, Document +local air = require("air") +local json = require("json") +local file = require("file") +file.activate_json(json) +local segment = {} +segment.setnames = {} +segment.name = "enforcer" +segment.settings = { + automod = { + list = { + + }, + status = false, + warn_limit = 3 + } +} + +if globals.enofrcer then + if globals.enforcer.setnames then + segment.setnames = globals.enforcer.setnames + end + if globals.enforcer.settings then + segment.settings = global.enforcer.settings + end +end +segment.warns = file.readJSON("./servers/"..id.."/warns.json",{}) + +events:on("serverSaveConfig",function() + if not globals.enforcer then + globals.enforcer = {} + end + globals.enforcer.setnames = segment.setnames + globals.enforcer.settings = segment.settings + file.writeJSON("./servers/"..id.."/warns.json",segment.warns) +end) + +local warn = function(ID,reason) + local guild = client:getGuild(id) + local member = guild:getMember(tostring(ID)) + if not segment.warns[tostring(ID)] then segment.warns[tostring(ID)] = {} end + table.insert(segment.warns[tostring(ID)],1,reason) + if segment.settings.warn_limit and (#segment.warns[tostring(ID)] >= segment.settings.warn_limit) and guild:getMember(tostring(ID)) then + if segment.settings.warn_punishment == "kick" then + member:kick("Warning quota exceeded.") + elseif segment.settings.warn_punishment == "ban" then + member:ban("Warning quota exceeded.",segment.settings.ban_days) + end + end + _ = (client:getUser(tostring(ID)) and client:getUser(tostring(ID)):send("__You have been warned.__\nReason: "..reason)) + signals:emit("warn",function(args) + if args[1] and member.name:find(args[1],1,true) then + return true + elseif not args[1] then + return true + else + return false + end + end,{ + user = member.id, + name = member.name + }) +end + +segment.commands = { + ["change-name"] = { + help = {embed = { + title = "Enforce a name upon a specific user", + description = "Whenever the user attempts to change their name, it will be changed back", + fields = { + {name = "Usage: ",value = "change-name "}, + {name = "Perms: ",value = "manageNicknames"} + } + }}, + perms = { + perms = { + "manageNicknames" + } + }, + args = { + "member", + "string" + }, + exec = function(msg,args,opts) + name = args[2] + args[1]:setNickname(name) + segment.setnames[tostring(args[1].id)] = name + msg:reply("Now assigning an enforced name upon "..args[1].name) + end + }, + ["reset-name"] = { + help = {embed = { + title = "Stop enforcing a name upon a user", + description = "Reverses the effect of ``change-name``", + fields = { + {name = "Usage: ",value = "reset-name"}, + {name = "Perms: ",value = "manageNicknames"} + } + }}, + perms = { + perms = { + "manageNicknames" + } + }, + args = { + "member" + }, + exec = function(msg,args,opts) + if segment.setnames[tostring(args[1].id)] then + segment.setnames[tostring(args[1].id)] = nil + args[1]:setNickname(nil) + msg:reply("No longer tracking "..args[1].name) + else + msg:reply("This user haven't been assigned an enforced name") + end + end + }, + ["wipe"] = { + help = {embed={ + title = "Wipe user messages", + description = "Searches and deletes all messages of a specific user in a specified range", + fields = { + {name = "Usage: ",value = "wipe-user "}, + {name = "Perms: ",value = "manageMessages"} + } + }}, + perms = { + perms = { + "manageMessages" + } + }, + args = { + "number", + }, + exec = function(msg,args,opts) + if tonumber(args[1]) and tonumber(args[1]) > 101 then + msg:reply("Search limit is too high") + return + end + local messages = {} + msg.channel:getMessages(args[1]):forEach(function(v) messages[#messages+1] = v.id end) + msg.channel:bulkDelete(messages) + end + }, + ["wipe-user"] = { + help = {embed={ + title = "Wipe user messages", + description = "Searches and deletes all messages of a specific user in a specified range", + fields = { + {name = "Usage: ",value = "wipe-user "}, + {name = "Perms: ",value = "manageMessages"} + } + }}, + perms = { + perms = { + "manageMessages" + } + }, + args = { + "number", + "member" + }, + exec = function(msg,args,opts) + if tonumber(args[1]) and tonumber(args[1]) > 101 then + msg:reply("Search limit is too high") + return + end + local messages = {} + local target = args[2].user + msg.channel:getMessages(args[1]):forEach(function(v) + if v.author.id == target.id then + messages[#messages+1] = v.id + end + end) + msg.channel:bulkDelete(messages) + end + }, + ["wipe-pattern"] = { + help = {embed={ + title = "Wipe by pattern", + description = "Searches for a specific pattern in a range of messages, and wipes if certain conditions are met", + fields = { + {name = "Usage: ",value = "wipe-pattern "}, + {name = "Perms: ",value = "manageMessages"} + } + }}, + perms = { + perms = { + "manageMessages" + } + }, + args = { + "number", + "string" + }, + exec = function(msg,args,opts) + if tonumber(args[1]) and tonumber(args[1]) > 101 then + msg:reply("Search limit is too high") + return + end + local messages = {} + msg.channel:getMessages(args[1]):forEach(function(v) + if v.content:find(args[2],1,true) then + messages[#messages+1] = v.id + end + end) + msg.channel:bulkDelete(messages) + end + }, + ["kick"] = { + help = {embed={ + title = "Kick a member", + description = "Self-descriptive", + fields = { + {name = "Usage: ",value = "kick []"}, + {name = "Perms: ",value= "kickMembers"} + } + }}, + perms = { + perms = { + "kickMembers" + } + }, + args = { + "member" + }, + exec = function(msg,args,opts) + local member = args[1] + signals:emit("kick",function(args) + if args[1] and member.name:find(args[1],1,true) then + return true + elseif not args[1] then + return true + else + return false + end + end,{ + user = member.id, + name = member.name, + reason = args[2] + }) + member:kick(args[2]) + end + }, + ["ban"] = { + help = {embed={ + title = "Ban a member", + description = "Self-descriptive", + fields = { + {name = "Usage: ",value = "ban [ []]"}, + {name = "Perms: ",value= "banMembers"} + } + }}, + perms = { + perms = { + "banMembers" + } + }, + args = { + "member" + }, + exec = function(msg,args,opts) + local member = args[1] + signals:emit("kick",function(args) + if args[1] and member.name:find(args[1],1,true) then + return true + elseif not args[1] then + return true + else + return false + end + end,{ + user = member.id, + name = member.name, + reason = args[2], + days = args[3] + }) + member:ban(args[2],tonumber(args[3])) + end + }, + ["purge"] = { + help = {embed={ + title = "Purge bot messages", + description = "If a number is provided, the bot will search through that amount of messages, or through 100 of them by default", + fields = { + {name = "Usage: ",value = "ban [ []]"}, + {name = "Perms: ",value= "manageMessages"} + } + }}, + perms = { + perms = { + "manageMessages" + } + }, + exec = function(msg,args,opts) + local messages = {} + if tonumber(args[1]) and tonumber(args[1]) > 101 then + msg:reply("Search limit is too high") + return + end + msg.channel:getMessages(tonumber(args[1]) or 100):forEach(function(v) + if (v.author.id == client.user.id) or (v.content:find(globals.prefix)==1) then + messages[#messages+1] = v.id + end + end) + msg.channel:bulkDelete(messages) + end + }, + ["warn"] = { + help = {embed={ + title = "Warn a user", + descriptions = "Warnings by themselves don't do any punishment to the user, but they allow managing users", + fields = { + {name = "Usage: ",value = "warn "}, + {name = "Perms: ",value = "kickMembers"} + } + }}, + perms = { + perms = { + "kickMembers" + } + }, + args = { + "member", + "string" + }, + exec = function(msg,args,opts) + local reason = table.concat(args," ",2) + warn(args[1].id,reason) + msg:reply({embed = { + title = "User "..args[1].name.." warned", + description = "Reason: ```"..reason.."```" + }}) + end + }, + ["warns"] = { + help = {embed={ + title = "List warnings", + descriptions = "self-descriptive", + fields = { + {name = "Usage: ",value = "warns []"}, + {name = "Perms: ",value = "kickMembers"} + } + }}, + perms = { + perms = { + "kickMembers" + } + }, + args = { + "member", + }, + exec = function(msg,args,opts) + local page = (tonumber(args[2]) or 1)-1 + local new_embed = { + title = "Warnings for "..args[1].name, + fields = {} + } + if page < 0 then + new_embed.description = "Page "..page.." not found, reverting to first page" + page = 0 + end + if segment.warns[tostring(args[1].id)] and #segment.warns[tostring(args[1].id)] > 0 then + for I = 1+(page*5),5+(page*5) do + local warn = segment.warns[tostring(args[1].id)][I] + if warn then + table.insert(new_embed.fields,{name = "ID: "..tostring(I),value = warn}) + end + end + msg:reply({embed = new_embed}) + else + msg:reply("This user has no warnings") + end + end + }, + ["unwarn"] = { + help = {embed={ + title = "Revoke a warning issued to a user", + descriptions = "self-descriptive", + fields = { + {name = "Usage: ",value = "unwarn []"}, + {name = "Perms: ",value = "kickMembers"} + } + }}, + perms = { + perms = { + "kickMembers" + } + }, + args = { + "member", + }, + exec = function(msg,args,opts) + local warn_id = (tonumber(args[2]) or 1) + if segment.warns[tostring(args[1].id)][warn_id] then + table.remove(segment.warns[tostring(args[1].id)],warn_id) + msg:reply("Revoked warning #"..warn_id) + else + msg:reply("No warning with id "..warn_id) + end + end + }, + ["add-role"] = { + help = {embed={ + title = "Give some specific user a role", + descriptions = "self-descriptive", + fields = { + {name = "Usage: ",value = "unwarn "}, + {name = "Perms: ",value = "manageRoles"} + } + }}, + perms = { + perms = { + "manageRoles" + } + }, + args = { + "member", + "role" + }, + exec = function(msg,args,opts) + args[1]:addRole(tostring(args[2].id)) + end + }, + ["remove-role"] = { + help = {embed={ + title = "Revoke a role from a user", + descriptions = "self-descriptive", + fields = { + {name = "Usage: ",value = "remove-role "}, + {name = "Perms: ",value = "manageRoles"} + } + }}, + perms = { + perms = { + "manageRoles" + } + }, + args = { + "member", + "role" + }, + exec = function(msg,args,opts) + args[1]:removeRole(tostring(args[2].id)) + end + }, +} + +events:on("memberUpdate",function(member) + if segment.setnames[tostring(member.id)] and member.nickname ~= segment.setnames[tostring(member.id)] then + member:setNickname(segment.setnames[tostring(member.id)]) + end +end) + +--old automod code +--[[ +events:on("messageCreate",function(msg) + if segment.settings.automod.status then + local trigger = "" + for k,v in pairs(segment.settings.automod.list) do + if msg.content:find(v) and msg.author ~= client.user then + trigger = trigger..v.."," + end + end + if trigger ~= "" then + full_text,author = msg.content.."",msg.author + msg:delete() + msg.author:send("The words \""..trigger.."\" are banned on this server.\nThis is the text that these words were found in: ```"..full_text.."```") + if segment.settings.automod.punishment == "kick" then + msg.author:kick("Usage of banned words") + elseif segment.settings.automod.punishment == "warn" then + warn(msg.author.id,"Usage of banned words",msg.guild) + end + end + end +end) +]] +return segment diff --git a/todo-stuff/reactions/init.lua b/todo-stuff/reactions/init.lua new file mode 100644 index 0000000..bc2fab5 --- /dev/null +++ b/todo-stuff/reactions/init.lua @@ -0,0 +1,340 @@ +local segment = {} +local emulate = require("emulate")({ + client = client, + discordia = discordia, +}) +local file = require("file") +file.activate_json(require("json")) +local guild = client:getGuild(id) +segment.pivots = file.readJSON("./servers/"..id.."/reactions.json",{}) +local getEmoji = function(id) + local emoji = guild:getEmoji(id:match("(%d+)[^%d]*$")) + if emoji then + return emoji + else + return id + end +end + +local function count(tab) + local n = 0 + for k,v in pairs(tab) do + n = n + 1 + end + return n +end + +segment.commands = { + ["pivot"] = { + help = { + title = "Select a pivot message to manipulate", + description = "Pivot is like a message selector which allows easy reaction manipulations", + fields = { + {name = "Usage: ",value = "pivot "}, + {name = "Perms: ",valeu = "Administartor"} + } + }, + args = { + "messageLink" + }, + perms = { + perms = { + "administrator" + } + }, + exec = function(msg,args,opts) + if segment.pivot and count(segment.pivot.buttons) == 0 then + segment.pivots[segment.pivot.message] = nil + end + local message = args[1] + if not message then + msg:reply("Couldn't find message with id "..args[2]) + return nil + end + if not segment.pivots[message.id] then + segment.pivots[message.id] = {} + segment.pivots[message.id].message = message.id + segment.pivots[message.id].channel = message.channel.id + segment.pivots[message.id].buttons = {} + end + segment.pivot = segment.pivots[message.id] + msg:reply("Pivot message set to "..message.link) + end + }, + ["role-toggle"] = { + help = { + title = "Add a simple role switch to the pivot", + description = "Note: you cannot assign more than one role to a single reaction", + fields = { + {name = "Usage: ",value = "role-toggle "}, + {name = "Perms: ",value = "administrator"} + } + }, + args = { + "string", + "role", + }, + perms = { + perms = { + "administrator" + } + }, + exec = function(msg,args,opts) + if not segment.pivot then + msg:reply("Pivot not selected. Use "..globals.prefix.."pivot to select it and then try again") + return nil + end + local emoji = getEmoji(args[1]) + local channel = guild:getChannel(segment.pivot.channel) + if not channel then + msg:reply("Something went horribly wrong, but it's not your fault. This incident has been (hopefully) reported") + return nil + end + local message = channel:getMessage(segment.pivot.message) + if not message then + msg:reply("Something went horribly wrong, but it's not your fault. This incident has been (hopefully) reported") + return nil + end + local grabEmoji = function(reaction) + segment.pivot.buttons[tostring(reaction.emojiId or reaction.emojiName)] = { + type = "role-toggler", + role = tostring(args[2].id) + } + msg:reply("Role toggler added successfully") + end + message:removeReaction(emoji,client.user.id) + client:once("reactionAdd",grabEmoji) + if not message:addReaction(emoji) then + client:removeListener("reactionAdd",grabEmoji) + msg:reply("Couldn't add reaction - emoji might be invalid") + end + end + }, + ["remove-reaction"] = { + help = { + title = "Remove a reaction from a pivot", + description = "If you don't specify a reaction to remove, the entire pivot for the message is removed automatically", + fields = { + {name = "Usage: ",value = "remove-reaction "}, + {name = "Perms: ",value = "Administrator"} + } + }, + perms = { + perms = { + "administrator" + } + }, + exec = function(msg,args,opts) + local channel = guild:getChannel(segment.pivot.channel) + if not channel then + msg:reply("Something went horribly wrong, but it's not your fault. This incident has been (hopefully) reported") + return nil + end + local message = channel:getMessage(segment.pivot.message) + if not message then + msg:reply("Something went horribly wrong, but it's not your fault. This incident has been (hopefully) reported") + return nil + end + if args[1] then + local emoji = getEmoji(args[1]) + message:removeReaction(emoji,client.user.id) + segment.pivot.buttons[((type(emoji) == "table") and emoji.id) or emoji] = nil + msg:reply("Action successfully removed") + else + message:clearReactions() + segment.pivots[tostring(message.id)] = nil + segment.pivot = nil + msg:reply("Pivot successfully removed") + end + end + }, + ["toggle"] = { + help = { + title = "Add a toggle that runs specific commands", + description = "Note: you cannot assign more than one action to a single reaction \n``$user`` gets replaced with the id of the user that interacted with the reaction.", + fields = { + {name = "Usage: ",value = "toggle "}, + {name = "Perms: ",value = "administrator"} + } + }, + args = { + "string", + "string", + "string", + }, + perms = { + perms = { + "administrator" + } + }, + exec = function(msg,args,opts) + if not segment.pivot then + msg:reply("Pivot not selected. Use "..globals.prefix.."pivot to select it and then try again") + return nil + end + local emoji = getEmoji(args[1]) + local channel = guild:getChannel(segment.pivot.channel) + if not channel then + msg:reply("Something went horribly wrong, but it's not your fault. This incident has been (hopefully) reported") + return nil + end + local message = channel:getMessage(segment.pivot.message) + if not message then + msg:reply("Something went horribly wrong, but it's not your fault. This incident has been (hopefully) reported") + return nil + end + local grabEmoji = function(reaction) + segment.pivot.buttons[tostring(reaction.emojiId or reaction.emojiName)] = { + type = "toggler", + on = args[2], + off = args[3], + } + msg:reply("Toggler added successfully") + end + message:removeReaction(emoji,client.user.id) + client:once("reactionAdd",grabEmoji) + if not message:addReaction(emoji) then + client:removeListener("reactionAdd",grabEmoji) + msg:reply("Couldn't add reaction - emoji might be invalid") + end + end + }, + ["button"] = { + help = { + title = "Add a button that runs specific command when pressed", + description = "Note: you cannot assign more than one action to a single reaction \n``$user`` gets replaced with the id of the user that interacted with the reaction.", + fields = { + {name = "Usage: ",value = "button "}, + {name = "Perms: ",value = "administrator"} + } + }, + args = { + "string", + "string", + }, + perms = { + perms = { + "administrator" + } + }, + exec = function(msg,args,opts) + if not segment.pivot then + msg:reply("Pivot not selected. Use "..globals.prefix.."pivot to select it and then try again") + return nil + end + local emoji = getEmoji(args[1]) + local channel = guild:getChannel(segment.pivot.channel) + if not channel then + msg:reply("Something went horribly wrong, but it's not your fault. This incident has been (hopefully) reported") + return nil + end + local message = channel:getMessage(segment.pivot.message) + if not message then + msg:reply("Something went horribly wrong, but it's not your fault. This incident has been (hopefully) reported") + return nil + end + local grabEmoji = function(reaction) + segment.pivot.buttons[tostring(reaction.emojiId or reaction.emojiName)] = { + type = "button", + on = args[2], + } + msg:reply("Button added successfully") + end + message:removeReaction(emoji,client.user.id) + client:once("reactionAdd",grabEmoji) + if not message:addReaction(emoji) then + client:removeListener("reactionAdd",grabEmoji) + msg:reply("Couldn't add reaction - emoji might be invalid") + end + end + }, +} + + +local buttonOn = function(message,hash,userID) + if not message then + log("ERROR","Attempted to find a deleted message") + return + end + if segment.pivots[tostring(message.id)] and userID ~= client.user.id then + local current_pivot = segment.pivots[tostring(message.id)] + if current_pivot.buttons[tostring(hash)] then + local current_button = current_pivot.buttons[tostring(hash)] + local new_content + if current_button.on then + new_content = current_button.on:gsub("%$user",userID) + end + if current_button.type == "role-toggler" then + guild:getMember(userID):addRole(current_button.role) + end + if current_button.type == "toggler" then + emulate.send(message,{ + delete = function() end, + content = new_content + }) + end + if current_button.type == "button" then + emulate.send(message,{ + delete = function() end, + content = new_content + }) + end + end + end +end + +local buttonOff = function(message,hash,userID) + if not message then + log("ERROR","Attempted to find a deleted message") + return + end + if segment.pivots[tostring(message.id)] and userID ~= client.user.id then + local current_pivot = segment.pivots[tostring(message.id)] + if current_pivot.buttons[tostring(hash)] then + local current_button = current_pivot.buttons[tostring(hash)] + local new_content + if current_button.off then + new_content = current_button.off:gsub("%$user",userID) + end + if current_button.type == "role-toggler" then + guild:getMember(userID):removeRole(current_button.role) + end + if current_button.type == "toggler" then + emulate.send(message,{ + delete = function() end, + content = new_content + }) + end + end + end +end + +events:on("reactionAdd",function(reaction,userID) + local message = reaction.message + local hash = tostring(reaction.emojiId or reaction.emojiName) + buttonOn(message,hash,userID) +end) + +events:on("reactionRemove",function(reaction,userID) + local message = reaction.message + local hash = tostring(reaction.emojiId or reaction.emojiName) + buttonOff(message,hash,userID) +end) + +events:on("reactionAddUncached",function(channelId,messageId,hash,userId) + local message = client:getChannel(channelId):getMessage(messageId) + local hash = tostring(hash) + buttonOn(message,hash,userId) +end) + +events:on("reactionRemoveUncached",function(channelId,messageId,hash,userId) + local message = client:getChannel(channelId):getMessage(messageId) + local hash = tostring(hash) + buttonOff(message,hash,userId) +end) + +events:on("serverSaveConfig",function() + file.writeJSON("./servers/"..id.."/reactions.json",segment.pivots) +end) + +return segment diff --git a/todo-stuff/settings/init.lua b/todo-stuff/settings/init.lua new file mode 100644 index 0000000..78a3637 --- /dev/null +++ b/todo-stuff/settings/init.lua @@ -0,0 +1,120 @@ +segment = {} +local check_perms = require("check_perms") +local fake_server = { + id = id +} +local file = require("file") +file.activate_json(require("json")) +local overlay = file.readJSON("./servers/"..id.."/overlay.json",{}) +local cached_commands = {} +events:on("commandPoolUpdate",function() + local commands = plugins.get()["commands"] + for k,v in pairs(commands) do + if not cached_commands[k] then + if overlay[k] then + if not v.perms then + v.perms = {} + end + v.perms.users = overlay[k].users or v.perms.users + v.perms.roles = overlay[k].roles or v.perms.roles + end + cached_commands[k] = true + end + end + for k,v in pairs(cached_commands) do + if not commands[k] then + cached_commands[k] = nil + end + end +end) + +events:on("serverSaveConfig",function() + file.writeJSON("./servers/"..id.."/overlay.json",overlay) +end) + +segment.commands = { + ["rules"] = { + args = { + "string", + "string", + }, + perms = { + perms = { + "administrator" + } + } + exec = function(msg,args,opts) + local target,command = args[1],args[2] + local commands = plugins.get()["commands"] + if not commands[target] then + msg:reply("Target command not found") + return + end + local name = target + target = commands[target] + if command == "list" then + local roles = "```" + for k,v in pairs(target.perms.roles or {}) do + roles = roles..((v > 0 and "allow ") or (v < 0 and "disallow "))..k.."\n" + end + roles = roles.." ```" + local users = "```" + for k,v in pairs(target.perms.users or {}) do + users = users..((v > 0 and "allow ") or (v < 0 and "disallow "))..k.."\n" + end + users = users.." ```" + msg:reply({embed={ + title = "Custom permissions for command ``"..name.."``", + fields = { + {name = "Roles",value = roles}, + {name = "Users",value = users} + } + }}) + else + if not check_perms(fake_server,target,msg,require("discordia")) then + msg:reply("You don't have a high enough permission to change rules for this command") + return + end + local type,id = args[3],args[4] + if not id then + msg:reply("Type and ID are needed to create a proper rule") + end + if (type ~= "user") and (type ~= "role") then + msg:reply("Type can only be ``user`` or ``role``") + end + id = id:match("%d+") + local state = 0 + if command == "allow" then + state = 1 + elseif command == "disallow" then + state = -1 + elseif command == "reset" then + state = nil + end + if not overlay[name] then + overlay[name] = {} + overlay[name].users = {} + overlay[name].roles = {} + end + if not target.perms then + target.perms = {} + end + if type == "user" then + if not target.perms.users then + target.perms.users = {} + end + target.perms.users[id] = state + overlay[name].users[id] = state + elseif type == "role" then + if not target.perms.roles then + target.perms.roles = {} + end + target.perms.roles[id] = state + overlay[name].roles[id] = state + end + msg:reply("Changes applied.") + end + end + } +} +return segment diff --git a/todo-stuff/tasks/init.lua b/todo-stuff/tasks/init.lua new file mode 100644 index 0000000..7c6d488 --- /dev/null +++ b/todo-stuff/tasks/init.lua @@ -0,0 +1,362 @@ +--haha yes time to make cron in lua +local segment = {} +segment.help = "Add tasks to be executed later" +local file = require("file") +file.activate_json(require("json")) +local utils = require("bot_utils") +local emulate = require("emulate")({ + client = client, + discordia = discordia +}) +segment.name = "task" +local preload_tab = file.readJSON("./servers/"..id.."/crontab.json",{}) +segment.tab = {} +globals.utc = globals.utc or 0 +globals.utc_minutes = globals.utc_minutes or 0 +segment.coroutines = {} +local absolute_fake_generator = require("absolute_fake") +local function cronTime(time) + if type(time) ~= "string" then + return false + end + local mask = {60,24,32,13} + local tokens = {} + local err = false + time:gsub("[%d%*]+",function(c) table.insert(tokens,c) end,4) + for I = 1,4 do --check if date/time format matches + if not ((tokens[I]:match("%d+") and tonumber(tokens[I]) < mask[I]) or ((tokens[I] == "*") and (I > 1))) then + err = true + end + end + if not err then + return tokens + else + return nil + end +end + +local function getFakeMessageOf(channel,member,content) + return absolute_fake_generator(client,discordia,member,channel,id,content) +end + +local function addEventTask(task) + if task.event and task.channel and task.member then + task.coroutineID = math.random(1000000000,9999999999) + while segment.coroutines[task.coroutineID] do + task.coroutineID = math.random(1000000000,9999999999) + end + local taskID = #segment.tab+1 + segment.coroutines[task.coroutineID] = function(check,args) + local check_func = ((type(check) == "function") and check) or (function() return true end) + if (not task.args) or (type(task.args) == "table" and check_func(task.args)) then + local content = task.task + for k,v in pairs(args or {}) do + content = content:gsub("%$"..k,tostring(v)) + end + local dupeable = getFakeMessageOf(task.channel,task.member) + if not dupeable then + log("ERROR","Failed to dupe a message properly") + return nil + end + emulate.send(dupeable,{ + content = content, + delete = function() end + }) + if task.once then + signals:removeListener(task.event,segment.coroutines[task.coroutineID]) + table.remove(segment.tab,taskID) + segment.coroutines[task.coroutineID] = nil + end + end + end + signals:on(task.event,segment.coroutines[task.coroutineID]) + table.insert(segment.tab,task) + return true + else + return false + end +end + +local function addTimeTask(task) + if type(task.time) == "table" then + table.insert(segment.tab,task) + return true + else + return false + end +end + +for k,v in pairs(preload_tab) do + if v.type == "event" then + addEventTask(v) + else + addTimeTask(v) + end +end + +segment.commands = { + ["task"] = { + help = {embed = { + title = "Add tasks", + description = "Tasks are like cron tasks, in a sense that you specify when to execute them and what command to execute", + fields = { + {name = "Usage: ",value = "task (\"minute hour day month\" or @ \"argument1\" \"argument2\") \"command\""}, + {name = "Perms: ",value = "Administrator"}, + {name = "Opts: ",value = [[ +--description=\"description here\" - self-descriptive +--once - remove the task after completion + ]]}, + {name = "Examples: ",value = [[ +``task "5 10 * *" "]]..globals.prefix..[[speak hi"`` -- sends "hi" to the current channel at 10:05 every day every month +``task "5 10 15 *" "]]..globals.prefix..[[speak hi"`` -- sends "hi" to the current channel at 10:05 every 15th day of the month +``task --once "5 10 15 *" "]]..globals.prefix..[[speak hi"`` -- same as before, except the task gets removed after sending the message +additional examples can be found at https://github.com/yessiest/SuppaBot/wiki/Tasks + ]]} + } + }}, + perms = { + perms = { + "administrator" + } + }, + args = { + "string", + "string" + }, + exec = function(msg,args,opts) + local command = args[#args] + if args[1]:match("^@%w+") then + local event = args[1]:match("^@(%w+)") + local conditions = utils.slice(args,2,#args-1) + local status = addEventTask({ + type = "event", + channel = tostring(msg.channel.id), + member = tostring(msg.member.id), + event = event, + args = conditions, + task = command, + description = opts["description"] or "", + once = opts["once"] + }) + if status then + msg:reply("Task "..(#segment.tab).." added") + else + msg:reply("Failed to add task") + end + elseif args[1]:match("^ ?%d+ [%d*]+ [%d*]+ [%d*]+ ?$") then + local status = addTimeTask({ + type = "time", + time = cronTime(args[1]:match("^ ?%d+ [%d*]+ [%d*]+ [%d*]+ ?$")), + channel = tostring(msg.channel.id), + member = tostring(msg.member.id), + task = command, + description = opts["description"] or "", + once = opts["once"] + }) + if status then + msg:reply("Task "..(#segment.tab).." added") + else + msg:reply("Failed to add task") + end + else + msg:reply("Syntax error") + end + end + }, + ["tasks"] = { + help = {embed = { + title = "List all tasks", + description = "Bold white text is conditions for the task, code block is the command to be executed", + fields = { + {name = "Usage: ",value = "tasks"}, + {name = "Perms: ",value = "Administrator"}, + } + }}, + perms = { + perms = { + "administrator" + } + }, + exec = function(msg,args,opts) + msg:reply({embed = { + title = "Tasks: ", + fields = (function() + local output = {} + for k,v in pairs(segment.tab) do + if v.type == "event" then + table.insert(output,{ + name = tostring(k)..": @"..v.event.." "..table.concat(v.args," "), + value = "```"..v.task.."```\n"..tostring(v.description), + inline = true + }) + elseif v.type == "time" then + table.insert(output,{ + name = tostring(k)..": "..table.concat(v.time," "), + value = "```"..v.task.."```\n"..tostring(v.description), + inline = true + }) + end + end + return output + end)() + }}) + end + }, + ["task-remove"] = { + help = {embed = { + title = "Remove a task", + description = "That one is self-descriptive", + fields = { + {name = "Usage: ",value = "remove-task "}, + {name = "Perms: ",value = "Administrator"}, + } + }}, + perms = { + perms = { + "administrator" + } + }, + args = { + "number" + }, + exec = function(msg,args,opts) + if segment.tab[args[1]] then + local task = segment.tab[args[1]] + if task.type == "event" then + signals:removeListener(task.event,segment.coroutines[task.coroutineID]) + segment.coroutines[task.coroutineID] = nil + end + table.remove(segment.tab,args[2]) + msg:reply("Task "..args[1].." removed") + else + msg:reply("Task "..args[1].." not found") + end + end + }, + ["utc"] = { + help = {embed = { + title = "Set the UTC time offset", + description = "If your UTC time offset is x:45 or x:30 simply add \":30\" or \"45\" to the number accordingly", + fields = { + {name = "Usage: ",value = "utc [:]"}, + {name = "Perms: ",value = "Administrator"}, + } + }}, + perms = { + perms = { + "administrator" + } + }, + args = { + "string" + }, + exec = function(msg,args,opts) + if args[1]:match("^%d+$") then + globals.utc = tonumber(args[1]:match("^(%d+)$")) + msg:reply("UTC offset set") + elseif args[1]:match("^%d+:%d+$") then + globals.utc = tonumber(args[1]:match("^(%d+):%d+$")) + globals.utc_minutes = tonumber(args[1]:match("^%d+:(%d+)$")) + msg:reply("UTC offset set") + else + msg:reply("Invalid syntax") + end + end + }, + ["time"] = { + help = {embed = { + title = "View the internal bot time", + description = "If you've set a time offset previously, it will get accounted", + fields = { + {name = "Usage: ",value = "time"}, + {name = "Perms: ",value = "all"}, + } + }}, + exec = function(msg,args,opts) + local utc_time = os.date("%c",os.time()+(3600)*(globals.utc-4)+(60)*(globals.utc_minutes)) + msg:reply(utc_time) + end + } +} + +segment.unload = function() + for k,v in pairs(segment.tab) do + if v.type == "event" then + signals:removeListener(v.event,segment.coroutines[v.coroutineID]) + segment.coroutines[v.coroutineID] = nil + segment.tab[k] = nil + end + end +end + +local function check_time(date,crondate) + local mask = {"min","hour","day","month"} + local output = true + for I = 1,4 do + if not (tonumber(crondate[I]) == tonumber(date[mask[I]]) or crondate[I] == "*") then + output = false + end + end + return output +end + +events:on("messageCreate",function(msg) + signals:emit("message",function(args) + local output = true + if not args[1] then + output = true + elseif msg.content:find(args[1],1,true) then + output = true + else + output = false + end + if output and (tostring(msg.author.id) == tostring(client.user.id)) then + output = false + end + if output and (msg.emulated) then + output = false + end + if output and (not args[2]) then + output = true + elseif output and (msg.author.name:find(args[2],1,true)) then + output = true + else + output = false + end + return output + end,{ + user = msg.author.id, + content = msg.content, + name = msg.author.name, + ctxid = msg.id + }) + return +end) + +events:on("serverSaveConfig",function() + file.writeJSON("./servers/"..id.."/crontab.json",segment.tab) +end) + +events:on("clock",function() + if tonumber(os.time())%60 == 30 then + local utc_time = os.date("*t",os.time()+(3600)*(globals.utc-4)+(60)*(globals.utc_minutes)) + for k,v in pairs(segment.tab) do + if (v.type == "time") and check_time(utc_time,v.time) then + emulate_message = getFakeMessageOf(v.channel,v.member) + if emulate_message then + emulate.send(emulate_message,{ + content = v.task, + delete = function() end + }) + else + log("ERROR","There are no messages to dupe a new one from") + end + if v.once then + table.remove(segment.tab,k) + end + end + end + end +end) + +return segment