Initial commit

master
Jordan Snelling 2020-06-27 18:41:49 +01:00
parent 9329e09d5f
commit dfbeb1c041
25 changed files with 2804 additions and 2 deletions

30
.gitignore vendored Normal file
View File

@ -0,0 +1,30 @@
### macOS ###
*.DS_Store
.AppleDouble
.LSOverride
auth.txt
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
.vs/slnx.sqlite
.vs

View File

@ -1,2 +1,56 @@
# discord
A Minetest Discord bridge with flexibility.
# Discord for Minetest
____
######_Part of the SolarSail family._
### Notice:
This mod requires LuaJIT; attempts at running it as a standard Lua interpreter will result in Minetest failing to execute this mod.
### Preparation:
`git clone` or manually download a zip of this folder and extract it to `minetest/mods` resulting in `minetest/mods/discord`.
Install cURL. You can find this in your package manager, in the case of Windows, this is provided for you.
### Golang Bot Setup Procedure:
Install the latest version of [Golang](https://golang.org/).
Using the terminal, execute `go get github.com/bwmarrin/discordgo`
Afterwards, you can test that the Golang is installed and functioning correctly for your environment by running `go run main.go` inside the `discord` folder.
By default, the bot will complain of a missing `auth.txt` and generate an empty file for you.
The error, will always come out as this in the terminal:
```
$ go run main.go
Failed to load auth.txt, a file named auth.txt has been created.
The file should contain the following lines:
<Discord bot token>
<Discord text channel ID>
<Discord server ID>
```
The auth.txt contents should look like this example, but not exactly as the token, server ID and channel ID can differ dramatically:
```
abcdefghijklmop1234.5
1234567890987654321
0987654321234567890
```
### Minetest Mod Setup Procedure
Add this mod to the chosen world via `worldmods` or by including it from the `mods` directory.
Grant this mod insecure environment access via `security.trusted_mods = discord`.
### Licensing:
This mod (`main.go` and `init.lua`) is licensed as MIT.
Discord.lua is licensed as MIT and can be found here: https://github.com/videah/discord.lua
`luajit-request`'s license can be found at `discord/discord/luajit-request/LICENSE`

BIN
bin/curl.exe Normal file

Binary file not shown.

BIN
bin/libcurl.exe Normal file

Binary file not shown.

182
discord/class.lua Normal file
View File

@ -0,0 +1,182 @@
local middleclass = {
_VERSION = 'middleclass v3.1.0',
_DESCRIPTION = 'Object Orientation for Lua',
_URL = 'https://github.com/kikito/middleclass',
_LICENSE = [[
MIT LICENSE
Copyright (c) 2011 Enrique García Cota
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.
]]
}
local function _setClassDictionariesMetatables(aClass)
local dict = aClass.__instanceDict
dict.__index = dict
local super = aClass.super
if super then
local superStatic = super.static
setmetatable(dict, super.__instanceDict)
setmetatable(aClass.static, { __index = function(_,k) return rawget(dict,k) or superStatic[k] end })
else
setmetatable(aClass.static, { __index = function(_,k) return dict[k] end })
end
end
local function _setClassMetatable(aClass)
setmetatable(aClass, {
__tostring = function() return "class " .. aClass.name end,
__index = aClass.static,
__newindex = aClass.__instanceDict,
__call = function(self, ...) return self:new(...) end
})
end
local function _createClass(name, super)
local aClass = { name = name, super = super, static = {}, __mixins = {}, __instanceDict={} }
aClass.subclasses = setmetatable({}, {__mode = "k"})
_setClassDictionariesMetatables(aClass)
_setClassMetatable(aClass)
return aClass
end
local function _createLookupMetamethod(aClass, name)
return function(...)
local method = aClass.super[name]
assert( type(method)=='function', tostring(aClass) .. " doesn't implement metamethod '" .. name .. "'" )
return method(...)
end
end
local function _setClassMetamethods(aClass)
for _,m in ipairs(aClass.__metamethods) do
aClass[m]= _createLookupMetamethod(aClass, m)
end
end
local function _setDefaultInitializeMethod(aClass, super)
aClass.initialize = function(instance, ...)
return super.initialize(instance, ...)
end
end
local function _includeMixin(aClass, mixin)
assert(type(mixin)=='table', "mixin must be a table")
for name,method in pairs(mixin) do
if name ~= "included" and name ~= "static" then aClass[name] = method end
end
if mixin.static then
for name,method in pairs(mixin.static) do
aClass.static[name] = method
end
end
if type(mixin.included)=="function" then mixin:included(aClass) end
aClass.__mixins[mixin] = true
end
local Object = _createClass("Object", nil)
Object.static.__metamethods = { '__add', '__band', '__bor', '__bxor', '__bnot', '__call', '__concat',
'__div', '__eq', '__ipairs', '__idiv', '__le', '__len', '__lt', '__mod',
'__mul', '__pairs', '__pow', '__shl', '__shr', '__sub', '__tostring', '__unm' }
function Object.static:allocate()
assert(type(self) == 'table', "Make sure that you are using 'Class:allocate' instead of 'Class.allocate'")
return setmetatable({ class = self }, self.__instanceDict)
end
function Object.static:new(...)
local instance = self:allocate()
instance:initialize(...)
return instance
end
function Object.static:subclass(name)
assert(type(self) == 'table', "Make sure that you are using 'Class:subclass' instead of 'Class.subclass'")
assert(type(name) == "string", "You must provide a name(string) for your class")
local subclass = _createClass(name, self)
_setClassMetamethods(subclass)
_setDefaultInitializeMethod(subclass, self)
self.subclasses[subclass] = true
self:subclassed(subclass)
return subclass
end
function Object.static:subclassed(other) end
function Object.static:isSubclassOf(other)
return type(other) == 'table' and
type(self) == 'table' and
type(self.super) == 'table' and
( self.super == other or
type(self.super.isSubclassOf) == 'function' and
self.super:isSubclassOf(other)
)
end
function Object.static:include( ... )
assert(type(self) == 'table', "Make sure you that you are using 'Class:include' instead of 'Class.include'")
for _,mixin in ipairs({...}) do _includeMixin(self, mixin) end
return self
end
function Object.static:includes(mixin)
return type(mixin) == 'table' and
type(self) == 'table' and
type(self.__mixins) == 'table' and
( self.__mixins[mixin] or
type(self.super) == 'table' and
type(self.super.includes) == 'function' and
self.super:includes(mixin)
)
end
function Object:initialize() end
function Object:__tostring() return "instance of " .. tostring(self.class) end
function Object:isInstanceOf(aClass)
return type(self) == 'table' and
type(self.class) == 'table' and
type(aClass) == 'table' and
( aClass == self.class or
type(aClass.isSubclassOf) == 'function' and
self.class:isSubclassOf(aClass)
)
end
function middleclass.class(name, super, ...)
super = super or Object
return super:subclass(name, ...)
end
middleclass.Object = Object
setmetatable(middleclass, { __call = function(_, ...) return middleclass.class(...) end })
return middleclass

299
discord/client.lua Normal file
View File

@ -0,0 +1,299 @@
-- This code is licensed under the MIT Open Source License.
-- Copyright (c) 2015 Ruairidh Carmichael - ruairidhcarmichael@live.co.uk
-- 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.
-------------------------------------
-- Discord Client Class
-- @module Client
local path = (...):match('(.-)[^%.]+$')
local request = require(path .. 'wrapper')
local class = require(path .. 'class')
local json = require(path .. 'json')
local endpoints = require(path .. 'endpoints')
local util = require(path .. 'utils')
local Message = require(path .. 'message')
print('Loaded client')
local Client = class('ClientObject')
--- Internally initialize's the Client class.
--- Please use Client:new(options) instead.
-- @param options Options table (Currently useless.)
function Client:initialize(options)
self.isLoggedIn = false
self.options = options
self.token = ''
self.email = ''
self.user = {}
self.headers = {}
self.headers['authorization'] = self.token
self.headers['Content-Type'] = 'application/json'
self.callbacks = {}
self.socket = nil
end
--- Logs the Client into Discord's servers.
--- This is required to use most of the Clients functions.
-- @param email E-mail Address of the account to log in.
-- @param password Password of the account to log in.
-- (WARNING: Do NOT store this password in a GitHub repo or anything of the sorts.)
-- @return True or False depending on success.
function Client:login(email, password)
local payload = {
email = email,
password = password
}
local response = request.send(endpoints.login, 'POST', payload, self.headers)
local success = util.responseIsSuccessful(response)
if success then
self.token = json.decode(response.body).token
self.isLoggedIn = true
self.headers['authorization'] = self.token
self.email = email
self.user = self:getCurrentUser()
end
return self.token
end
function Client:loginWithToken(token)
self.token = token
self.isLoggedIn = true
self.headers['authorization'] = "Bot "..token
self.user = self:getCurrentUser()
return token
end
--- Logs the Client out of the Discord server.
--- (This is currently useless/does nothing)
-- @param email E-mail Address of the account to log in.
-- @param password Password of the account to log in.
-- (WARNING: Do NOT store this password in a GitHub repo or anything of the sorts.)
-- @return True or False depending on success.
function Client:logout()
if self.isLoggedIn then
local payload = {
token = self.token
}
local response = request.send(endpoints.login, 'POST', payload)
local success = util.responseIsSuccessful(response)
if success then
self.isLoggedIn = false
self.token = nil
end
return success
else
return false
end
end
--- Gets the current authentication token.
--- (Only if logged in of course.)
-- @return Authentication Token
function Client:getToken()
if self.isLoggedIn then
return self.token
else
return nil
end
end
--- Gets the current Gateway we are connected to.
--- (Only if logged in of course.)
-- @return Gateway URL
function Client:getGateway()
if self.isLoggedIn then
local response = request.send(endpoints.gateway, 'GET', nil, self.headers)
if util.responseIsSuccessful(response) then
return json.decode(response.body).url
else
return nil
end
end
end
--- Gets the current User information.
--- (Only if logged in of course.)
-- @return User Table
function Client:getCurrentUser()
if self.isLoggedIn then
local response = request.send(endpoints.users .. '/@me', 'GET', nil, self.headers)
if util.responseIsSuccessful(response) then
return json.decode(response.body)
else
return nil
end
else
return nil
end
end
--- Gets a table of Servers the current User is connected to.
--- (Only if logged in of course.)
-- @return Server Table
function Client:getServerList()
if self.isLoggedIn then
local response = request.send(endpoints.users .. '/@me/guilds', 'GET', nil, self.headers)
if util.responseIsSuccessful(response) then
self.user = json.decode(response.body)
return json.decode(response.body)
else
return nil
end
else
return nil
end
end
--- Gets a table of Channels from the provided Server id.
--- (Only if logged in of course.)
-- @param id Server ID
-- @return Channel Table
function Client:getChannelList(id)
if self.isLoggedIn then
local response = request.send(endpoints.servers .. '/' .. id .. '/channels', 'GET', nil, self.headers)
if util.responseIsSuccessful(response) then
return json.decode(response.body)
else
return nil
end
else
return nil
end
end
--- Sends a Message from the Client to a Channel.
--- (Only if logged in of course.)
-- @param message Message to be sent.
-- @param id ID for Channel to send to.
-- @return Message Class
function Client:sendMessage(message, id)
if self.isLoggedIn then
local payload = {
content = tostring(message)
}
local response = request.send(endpoints.channels .. '/' .. id .. '/messages', 'POST', payload, self.headers)
return Message:new(json.decode(response.body), self.token)
else
return nil
end
end
--- Edits a sent Message.
--- (Only if logged in of course.)
-- @param message The new message to replace the old one.
-- @param msgClass Message Class to edit.
-- @return Message Class
function Client:editMessage(message, msgClass)
if self.isLoggedIn then
msgClass:edit(message)
end
end
--- Deletes a sent Message.
--- (Only if logged in of course.)
-- @param msgClass Message Class to delete.
-- @return Message Class
function Client:deleteMessage(msgClass)
if self.isLoggedIn then
msgClass:delete()
end
end
--- Starts the Client loop.
--- (Does nothing of real use at the moment, will interact with WebSockets in the future)
function Client:run()
if self.isLoggedIn then
while true do
if self.callbacks.think then self.callbacks.think() end
end
end
end
return Client

35
discord/endpoints.lua Normal file
View File

@ -0,0 +1,35 @@
-- This code is licensed under the MIT Open Source License.
-- Copyright (c) 2015 Ruairidh Carmichael - ruairidhcarmichael@live.co.uk
-- 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.
local endp = {}
endp.base = 'https://discordapp.com'
endp.apiBase = endp.base .. '/api'
endp.gateway = endp.apiBase .. '/gateway'
endp.users = endp.apiBase .. '/users'
endp.register = endp.apiBase .. '/auth/register'
endp.login = endp.apiBase .. '/auth/login'
endp.logout = endp.apiBase .. '/auth/logout'
endp.servers = endp.apiBase .. '/guilds'
endp.channels = endp.apiBase .. '/channels'
return endp

35
discord/init.lua Normal file
View File

@ -0,0 +1,35 @@
-- This code is licensed under the MIT Open Source License.
-- Copyright (c) 2015 Ruairidh Carmichael - ruairidhcarmichael@live.co.uk
-- 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.
local path = (...):match('(.-)[^%.]+$')
local discord = {
_VERSION = '0.0.1',
_DESCRIPTION = 'Lua API Wrapper for Discord'
}
discord.Client = require(path .. 'client')
discord.Message = require(path .. 'message')
return discord

375
discord/json.lua Normal file
View File

@ -0,0 +1,375 @@
--
-- json.lua
--
-- Copyright (c) 2015 rxi
--
-- This library is free software; you can redistribute it and/or modify it
-- under the terms of the MIT license. See LICENSE for details.
--
local json = { _version = "0.1.0" }
-- Encode
local encode
local escape_char_map = {
[ "\\" ] = "\\\\",
[ "\"" ] = "\\\"",
[ "\b" ] = "\\b",
[ "\f" ] = "\\f",
[ "\n" ] = "\\n",
[ "\r" ] = "\\r",
[ "\t" ] = "\\t",
}
local escape_char_map_inv = { [ "\\/" ] = "/" }
for k, v in pairs(escape_char_map) do
escape_char_map_inv[v] = k
end
local function escape_char(c)
return escape_char_map[c] or string.format("\\u%04x", c:byte())
end
local function encode_nil(val)
return "null"
end
local function encode_table(val, stack)
local res = {}
stack = stack or {}
-- Circular reference?
if stack[val] then error("circular reference") end
stack[val] = true
if val[1] ~= nil or next(val) == nil then
-- Treat as array -- check keys are valid and it is not sparse
local n = 0
for k in pairs(val) do
if type(k) ~= "number" then
error("invalid table: mixed or invalid key types")
end
n = n + 1
end
if n ~= #val then
error("invalid table: sparse array")
end
-- Encode
for i, v in ipairs(val) do
table.insert(res, encode(v, stack))
end
stack[val] = nil
return "[" .. table.concat(res, ",") .. "]"
else
-- Treat as an object
for k, v in pairs(val) do
if type(k) ~= "string" then
error("invalid table: mixed or invalid key types")
end
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
end
stack[val] = nil
return "{" .. table.concat(res, ",") .. "}"
end
end
local function encode_string(val)
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
end
local function encode_number(val)
-- Check for NaN, -inf and inf
if val ~= val or val <= -math.huge or val >= math.huge then
error("unexpected number value '" .. tostring(val) .. "'")
end
return string.format("%.14g", val)
end
local type_func_map = {
[ "nil" ] = encode_nil,
[ "table" ] = encode_table,
[ "string" ] = encode_string,
[ "number" ] = encode_number,
[ "boolean" ] = tostring,
}
encode = function(val, stack)
local t = type(val)
local f = type_func_map[t]
if f then
return f(val, stack)
end
error("unexpected type '" .. t .. "'")
end
function json.encode(val)
return ( encode(val) )
end
-- Decode
local parse
local function create_set(...)
local res = {}
for i = 1, select("#", ...) do
res[ select(i, ...) ] = true
end
return res
end
local space_chars = create_set(" ", "\t", "\r", "\n")
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
local literals = create_set("true", "false", "null")
local literal_map = {
[ "true" ] = true,
[ "false" ] = false,
[ "null" ] = nil,
}
local function next_char(str, idx, set, negate)
for i = idx, #str do
if set[str:sub(i, i)] ~= negate then
return i
end
end
return #str + 1
end
local function decode_error(str, idx, msg)
local line_count = 1
local col_count = 1
for i = 1, idx - 1 do
col_count = col_count + 1
if str:sub(i, i) == "\n" then
line_count = line_count + 1
col_count = 1
end
end
error( string.format("%s at line %d col %d", msg, line_count, col_count) )
end
local function codepoint_to_utf8(n)
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
local f = math.floor
if n <= 0x7f then
return string.char(n)
elseif n <= 0x7ff then
return string.char(f(n / 64) + 192, n % 64 + 128)
elseif n <= 0xffff then
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
elseif n <= 0x10ffff then
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
f(n % 4096 / 64) + 128, n % 64 + 128)
end
error( string.format("invalid unicode codepoint '%x'", n) )
end
local function parse_unicode_escape(s)
local n1 = tonumber( s:sub(3, 6), 16 )
local n2 = tonumber( s:sub(9, 12), 16 )
-- Surrogate pair?
if n2 then
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
else
return codepoint_to_utf8(n1)
end
end
local function parse_string(str, i)
local has_unicode_escape = false
local has_surrogate_escape = false
local has_escape = false
local last
for j = i + 1, #str do
local x = str:byte(j)
if x < 32 then
decode_error(str, j, "control character in string")
end
if last == 92 then -- "\\" (escape char)
if x == 117 then -- "u" (unicode escape sequence)
local hex = str:sub(j + 1, j + 5)
if not hex:find("%x%x%x%x") then
decode_error(str, j, "invalid unicode escape in string")
end
if hex:find("^[dD][89aAbB]") then
has_surrogate_escape = true
else
has_unicode_escape = true
end
else
local c = string.char(x)
if not escape_chars[c] then
decode_error(str, j, "invalid escape char '" .. c .. "' in string")
end
has_escape = true
end
last = nil
elseif x == 34 then -- '"' (end of string)
local s = str:sub(i + 1, j - 1)
if has_surrogate_escape then
s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape)
end
if has_unicode_escape then
s = s:gsub("\\u....", parse_unicode_escape)
end
if has_escape then
s = s:gsub("\\.", escape_char_map_inv)
end
return s, j + 1
else
last = x
end
end
decode_error(str, i, "expected closing quote for string")
end
local function parse_number(str, i)
local x = next_char(str, i, delim_chars)
local s = str:sub(i, x - 1)
local n = tonumber(s)
if not n then
decode_error(str, i, "invalid number '" .. s .. "'")
end
return n, x
end
local function parse_literal(str, i)
local x = next_char(str, i, delim_chars)
local word = str:sub(i, x - 1)
if not literals[word] then
decode_error(str, i, "invalid literal '" .. word .. "'")
end
return literal_map[word], x
end
local function parse_array(str, i)
local res = {}
local n = 1
i = i + 1
while 1 do
local x
i = next_char(str, i, space_chars, true)
-- Empty / end of array?
if str:sub(i, i) == "]" then
i = i + 1
break
end
-- Read token
x, i = parse(str, i)
res[n] = x
n = n + 1
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "]" then break end
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
end
return res, i
end
local function parse_object(str, i)
local res = {}
i = i + 1
while 1 do
local key, val
i = next_char(str, i, space_chars, true)
-- Empty / end of object?
if str:sub(i, i) == "}" then
i = i + 1
break
end
-- Read key
if str:sub(i, i) ~= '"' then
decode_error(str, i, "expected string for key")
end
key, i = parse(str, i)
-- Read ':' delimiter
i = next_char(str, i, space_chars, true)
if str:sub(i, i) ~= ":" then
decode_error(str, i, "expected ':' after key")
end
i = next_char(str, i + 1, space_chars, true)
-- Read value
val, i = parse(str, i)
-- Set
res[key] = val
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "}" then break end
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
end
return res, i
end
local char_func_map = {
[ '"' ] = parse_string,
[ "0" ] = parse_number,
[ "1" ] = parse_number,
[ "2" ] = parse_number,
[ "3" ] = parse_number,
[ "4" ] = parse_number,
[ "5" ] = parse_number,
[ "6" ] = parse_number,
[ "7" ] = parse_number,
[ "8" ] = parse_number,
[ "9" ] = parse_number,
[ "-" ] = parse_number,
[ "t" ] = parse_literal,
[ "f" ] = parse_literal,
[ "n" ] = parse_literal,
[ "[" ] = parse_array,
[ "{" ] = parse_object,
}
parse = function(str, idx)
local chr = str:sub(idx, idx)
local f = char_func_map[chr]
if f then
return f(str, idx)
end
decode_error(str, idx, "unexpected character '" .. chr .. "'")
end
function json.decode(str)
if type(str) ~= "string" then
error("expected argument of type string, got " .. type(str))
end
return ( parse(str, next_char(str, 1, space_chars, true)) )
end
return json

View File

@ -0,0 +1,11 @@
Copyright (c) 2014 Lucien Greathouse
This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software.
Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.

View File

@ -0,0 +1,363 @@
--[[
LuaJIT-Request
Lucien Greathouse
Wrapper for LuaJIT-cURL for easy HTTP(S) requests.
Copyright (c) 2016 Lucien Greathouse
This software is provided 'as-is', without any express
or implied warranty. In no event will the authors be held
liable for any damages arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, andto alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must
not be misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
]]
local path = (...):gsub("%.init$", ""):match("%.?(.-)$") .. "."
local ffi = require("ffi")
local curl = require(path .. "luajit-curl")
local request
local function url_encode(str)
if (str) then
str = str:gsub("\n", "\r\n")
str = str:gsub("([^%w %-%_%.%~])", function(c)
return string.format ("%%%02X", string.byte(c))
end)
str = str:gsub(" ", "%%20")
end
return str
end
local function cookie_encode(str, name)
str = str:gsub("[,;%s]", "")
if (name) then
str = str:gsub("=", "")
end
return str
end
local auth_map = {
BASIC = ffi.cast("long", curl.CURLAUTH_BASIC),
DIGEST = ffi.cast("long", curl.CURLAUTH_DIGEST),
NEGOTIATE = ffi.cast("long", curl.CURLAUTH_NEGOTIATE)
}
local errors = {
unknown = 0,
timeout = 1,
connect = 2,
resolve_host = 3
}
local code_map = {
[curl.CURLE_OPERATION_TIMEDOUT] = {
errors.timeout, "Connection timed out"
},
[curl.CURLE_COULDNT_RESOLVE_HOST] = {
errors.resolve_host, "Couldn't resolve host"
},
[curl.CURLE_COULDNT_CONNECT] = {
errors.connect, "Couldn't connect to host"
}
}
request = {
error = errors,
version = "2.4.0",
version_major = 2,
version_minor = 4,
version_patch = 0,
--[[
Send an HTTP(S) request to the URL at 'url' using the HTTP method 'method'.
Use the 'args' parameter to optionally configure the request:
- method: HTTP method to use. Defaults to "GET", but can be any HTTP verb like "POST" or "PUT"
- headers: Dictionary of additional HTTP headers to send with request
- data: Dictionary or string to send as request body
- cookies: Dictionary table of cookies to send
- timeout: How long to wait for the connection to be made before giving up
- allow_redirects: Whether or not to allow redirection. Defaults to true
- body_stream_callback: A method to call with each piece of the response body.
- header_stream_callback: A method to call with each piece of the resulting header.
- transfer_info_callback: A method to call with transfer progress data.
- auth_type: Authentication method to use. Defaults to "none", but can also be "basic", "digest" or "negotiate"
- username: A username to use with authentication. 'auth_type' must also be specified.
- password: A password to use with authentication. 'auth_type' must also be specified.
- files: A dictionary of file names to their paths on disk to upload via stream.
If both body_stream_callback and header_stream_callback are defined, a boolean true will be returned instead of the following object.
The return object is a dictionary with the following members:
- code: The HTTP status code the response gave. Will not exist if header_stream_callback is defined above.
- body: The body of the response. Will not exist if body_stream_callback is defined above.
- headers: A dictionary of headers and their values. Will not exist if header_stream_callback is defined above.
- headers_raw: A raw string containing the actual headers the server sent back. Will not exist if header_stream_callback is defined above.
- set_cookies: A dictionary of cookies given by the "Set-Cookie" header from the server. Will not exist if the server did not set any cookies.
]]
send = function(url, args)
local handle = curl.curl_easy_init()
local header_chunk
local out_buffer
local headers_buffer
args = args or {}
local callbacks = {}
local gc_handles = {}
curl.curl_easy_setopt(handle, curl.CURLOPT_URL, url)
curl.curl_easy_setopt(handle, curl.CURLOPT_SSL_VERIFYPEER, 1)
curl.curl_easy_setopt(handle, curl.CURLOPT_SSL_VERIFYHOST, 2)
if (args.method) then
local method = string.upper(tostring(args.method))
if (method == "GET") then
curl.curl_easy_setopt(handle, curl.CURLOPT_HTTPGET, 1)
elseif (method == "POST") then
curl.curl_easy_setopt(handle, curl.CURLOPT_POST, 1)
else
curl.curl_easy_setopt(handle, curl.CURLOPT_CUSTOMREQUEST, method)
end
end
if (args.headers) then
for key, value in pairs(args.headers) do
header_chunk = curl.curl_slist_append(header_chunk, tostring(key) .. ":" .. tostring(value))
end
curl.curl_easy_setopt(handle, curl.CURLOPT_HTTPHEADER, header_chunk)
end
if (args.auth_type) then
local auth = string.upper(tostring(args.auth_type))
if (auth_map[auth]) then
curl.curl_easy_setopt(handle, curl.CURLOPT_HTTPAUTH, auth_map[auth])
curl.curl_easy_setopt(handle, curl.CURLOPT_USERNAME, tostring(args.username))
curl.curl_easy_setopt(handle, curl.CURLOPT_PASSWORD, tostring(args.password or ""))
elseif (auth ~= "NONE") then
error("Unsupported authentication type '" .. auth .. "'")
end
end
if (args.body_stream_callback) then
local callback = ffi.cast("curl_callback", function(data, size, nmeb, user)
args.body_stream_callback(ffi.string(data, size * nmeb))
return size * nmeb
end)
table.insert(callbacks, callback)
curl.curl_easy_setopt(handle, curl.CURLOPT_WRITEFUNCTION, callback)
else
out_buffer = {}
local callback = ffi.cast("curl_callback", function(data, size, nmeb, user)
table.insert(out_buffer, ffi.string(data, size * nmeb))
return size * nmeb
end)
table.insert(callbacks, callback)
curl.curl_easy_setopt(handle, curl.CURLOPT_WRITEFUNCTION, callback)
end
if (args.header_stream_callback) then
local callback = ffi.cast("curl_callback", function(data, size, nmeb, user)
args.header_stream_callback(ffi.string(data, size * nmeb))
return size * nmeb
end)
table.insert(callbacks, callback)
curl.curl_easy_setopt(handle, curl.CURLOPT_HEADERFUNCTION, callback)
else
headers_buffer = {}
local callback = ffi.cast("curl_callback", function(data, size, nmeb, user)
table.insert(headers_buffer, ffi.string(data, size * nmeb))
return size * nmeb
end)
table.insert(callbacks, callback)
curl.curl_easy_setopt(handle, curl.CURLOPT_HEADERFUNCTION, callback)
end
if (args.transfer_info_callback) then
local callback = ffi.cast("curl_xferinfo_callback", function(client, dltotal, dlnow, ultotal, ulnow)
args.transfer_info_callback(tonumber(dltotal), tonumber(dlnow), tonumber(ultotal), tonumber(ulnow))
return 0
end)
table.insert(callbacks, callback)
curl.curl_easy_setopt(handle, curl.CURLOPT_NOPROGRESS, 0)
curl.curl_easy_setopt(handle, curl.CURLOPT_XFERINFOFUNCTION, callback)
end
if (args.follow_redirects == nil) then
curl.curl_easy_setopt(handle, curl.CURLOPT_FOLLOWLOCATION, true)
else
curl.curl_easy_setopt(handle, curl.CURLOPT_FOLLOWLOCATION, not not args.follow_redirects)
end
if (args.data) then
if (type(args.data) == "table") then
local buffer = {}
for key, value in pairs(args.data) do
table.insert(buffer, ("%s=%s"):format(url_encode(key), url_encode(value)))
end
curl.curl_easy_setopt(handle, curl.CURLOPT_POSTFIELDS, table.concat(buffer, "&"))
else
curl.curl_easy_setopt(handle, curl.CURLOPT_POSTFIELDS, tostring(args.data))
end
end
local post
if (args.files) then
post = ffi.new("struct curl_httppost*[1]")
local lastptr = ffi.new("struct curl_httppost*[1]")
for key, value in pairs(args.files) do
local file = ffi.new("char[?]", #value, value)
table.insert(gc_handles, file)
local res = curl.curl_formadd(
post, lastptr,
ffi.new("int", curl.CURLFORM_COPYNAME), key,
ffi.new("int", curl.CURLFORM_FILE), file,
ffi.new("int", curl.CURLFORM_END)
)
end
curl.curl_easy_setopt(handle, curl.CURLOPT_HTTPPOST, post[0])
end
-- Enable the cookie engine
curl.curl_easy_setopt(handle, curl.CURLOPT_COOKIEFILE, "")
if (args.cookies) then
local cookie_out
if (type(args.cookies) == "table") then
local buffer = {}
for key, value in pairs(args.cookies) do
table.insert(buffer, ("%s=%s"):format(cookie_encode(key, true), cookie_encode(value)))
end
cookie_out = table.concat(buffer, "; ")
else
cookie_out = tostring(args.cookies)
end
curl.curl_easy_setopt(handle, curl.CURLOPT_COOKIE, cookie_out)
end
if (tonumber(args.timeout)) then
curl.curl_easy_setopt(handle, curl.CURLOPT_CONNECTTIMEOUT, tonumber(args.timeout))
end
local code = curl.curl_easy_perform(handle)
if (code ~= curl.CURLE_OK) then
local num = tonumber(code)
if (code_map[num]) then
return false, code_map[num][1], code_map[num][2]
end
return false, request.error.unknown, "Unknown error", num
end
local out
if (out_buffer or headers_buffer) then
local headers, status, parsed_headers, raw_cookies, set_cookies
if (headers_buffer) then
headers = table.concat(headers_buffer)
status = headers:match("%s+(%d+)%s+")
parsed_headers = {}
for key, value in headers:gmatch("\n([^:]+): *([^\r\n]*)") do
parsed_headers[key] = value
end
end
local cookielist = ffi.new("struct curl_slist*[1]")
curl.curl_easy_getinfo(handle, curl.CURLINFO_COOKIELIST, cookielist)
if cookielist[0] ~= nil then
raw_cookies, set_cookies = {}, {}
local cookielist = ffi.gc(cookielist[0], curl.curl_slist_free_all)
local cookie = cookielist
repeat
local raw = ffi.string(cookie[0].data)
table.insert(raw_cookies, raw)
local domain, subdomains, path, secure, expiration, name, value = raw:match("^(.-)\t(.-)\t(.-)\t(.-)\t(.-)\t(.-)\t(.*)$")
set_cookies[name] = value
cookie = cookie[0].next
until cookie == nil
end
out = {
body = table.concat(out_buffer),
headers = parsed_headers,
raw_cookies = raw_cookies,
set_cookies = set_cookies,
code = status,
raw_headers = headers
}
else
out = true
end
curl.curl_easy_cleanup(handle)
curl.curl_slist_free_all(header_chunk)
if (post) then
curl.curl_formfree(post[0])
end
gc_handles = {}
for i, v in ipairs(callbacks) do
v:free()
end
return out
end,
init = function()
curl.curl_global_init(curl.CURL_GLOBAL_ALL)
end,
close = function()
curl.curl_global_cleanup()
end
}
request.init()
return request

File diff suppressed because it is too large Load Diff

86
discord/message.lua Normal file
View File

@ -0,0 +1,86 @@
-- This code is licensed under the MIT Open Source License.
-- Copyright (c) 2015 Ruairidh Carmichael - ruairidhcarmichael@live.co.uk
-- 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.
-------------------------------------
-- Discord Message Class
-- @module Message
local path = (...):match('(.-)[^%.]+$')
local request = require(path .. 'wrapper')
local class = require(path .. 'class')
local json = require(path .. 'json')
local endpoints = require(path .. 'endpoints')
local util = require(path .. 'utils')
local Message = class('MessageObject')
--- Internally initialize's the Message class.
--- Please use Message:new(packet, token) instead.
-- @param packet Packet to be sent as the message.
-- @param token Authentication token from login.
function Message:initialize(packet, token)
self.packet = packet or {}
self.token = token or ''
self.headers = {}
self.headers['authorization'] = self.token
self.headers['Content-Type'] = 'application/json'
end
--- Edits a sent message.
-- @param msg The new message to replace the old one.
-- @return Response Packet received from Discord
function Message:edit(msg)
local payload = {
content = tostring(msg)
}
local response = request.send(endpoints.channels .. '/' .. self.packet.channel_id .. '/messages/' .. self.packet.id, 'PATCH', payload, self.headers)
if util.responseIsSuccessful(response) then
self.packet = json.decode(response.body)
end
return util.responseIsSuccessful(response)
end
--- Deletes a sent message.
-- @return Response Packet received from Discord
function Message:delete()
local response = request.send(endpoints.channels .. '/' .. self.packet.channel_id .. '/messages/' .. self.packet.id, 'DELETE', nil, self.headers)
if util.responseIsSuccessful(response) then
self = nil
end
return util.responseIsSuccessful(response)
end
return Message

36
discord/utils.lua Normal file
View File

@ -0,0 +1,36 @@
-- This code is licensed under the MIT Open Source License.
-- Copyright (c) 2015 Ruairidh Carmichael - ruairidhcarmichael@live.co.uk
-- 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.
-------------------------------------
-- Various Internal Utilitys
-- @module Utils
local utils = {}
--- Returns true if the response was successful.
-- @param response Response from Discord server.
-- @return True or False depending on response
function utils.responseIsSuccessful(response)
return tonumber(response.code) >= 200 and tonumber(response.code) < 300
end
return utils

42
discord/wrapper.lua Normal file
View File

@ -0,0 +1,42 @@
-- This code is licensed under the MIT Open Source License.
-- Copyright (c) 2015 Ruairidh Carmichael - ruairidhcarmichael@live.co.uk
-- 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.
local path = (...):match('(.-)[^%.]+$')
local request = require(path .. 'luajit-request.init')
local json = require(path .. 'json')
local requestWrapper = {}
function requestWrapper.send(endpoint, method, data, headers)
local payload = {}
if headers then payload['headers'] = headers end
if method then payload['method'] = method end
if data then payload['data'] = json.encode(data) end
return request.send(endpoint, payload)
end
return requestWrapper

BIN
dlls/libcurl.dll Normal file

Binary file not shown.

BIN
dlls/libeay32.dll Normal file

Binary file not shown.

BIN
dlls/libssh2.dll Normal file

Binary file not shown.

BIN
dlls/msvcr120.dll Normal file

Binary file not shown.

BIN
dlls/ssleay32.dll Normal file

Binary file not shown.

BIN
dlls/zlib1.dll Normal file

Binary file not shown.

110
init.lua Normal file
View File

@ -0,0 +1,110 @@
-- prototype discord bridge for minetest
local discord_token = ""
local channel_id = ""
local line = 1
for lines in io.lines(minetest.get_modpath("discord") .. "/auth.txt") do
if line == 1 then
discord_token = tostring(lines)
else
channel_id = tostring(lines)
break
end
line = line + 1
end
local ie = minetest.request_insecure_environment()
local old_require = require
require = ie.require
ie.package.path = package.path .. ";" .. minetest.get_modpath(minetest.get_current_modname()) .. "/?.lua"
local discord_c = require "discord.init"
require = old_require
discord = {}
mt_client = discord_c.Client:new()
discord.accepted_token = false
if mt_client:loginWithToken(discord_token) then
mt_client:sendMessage('[Server] Server started.', channel_id)
discord.accepted_token = true
end
function discord.send_message(message)
if discord.accepted_token then
mt_client:sendMessage(message, channel_id)
end
end
minetest.register_on_chat_message(function(name, message)
if discord.accepted_token then
mt_client:sendMessage("<**" .. name .. "**> " .. minetest.strip_colors(message), channel_id)
end
end)
minetest.register_on_joinplayer(function(player)
if discord.accepted_token then
mt_client:sendMessage("**" .. player:get_player_name() .. "**" .. " joined the game.", channel_id)
end
end)
minetest.register_on_leaveplayer(function(player)
if discord.accepted_token then
mt_client:sendMessage("**" .. player:get_player_name() .. "**" .. " left the game.", channel_id)
end
end)
minetest.register_on_shutdown(function()
if discord.accepted_token then
mt_client:sendMessage("[Server] Server shutting down.", channel_id)
end
end)
-- Destroy the file on game boot:
local log_file = minetest.get_modpath("discord").."/discord.log"
local chat = {}
local increment = 0
local discord_colour = "#7289DA"
os.remove(log_file)
local function get_discord_messages()
local log = io.open(log_file)
if log == nil then
else
log:close()
local num_lines = 0
for line in io.lines(log_file) do
-- Deserialise chat information coming from discord.go
local desel = minetest.deserialize(line)
if chat[num_lines] == nil then
if desel.message == "" then
-- Create an empty line if the deserialised message text is empty
-- we do this because uploading an image or file with no text causes
-- an anomalous message with an empty string
chat[num_lines] = ""
else
-- Colourise the [Discord] text and the <user_name> of the Discord member
-- according to their role.
chat[num_lines] = minetest.colorize(discord_colour, desel.server) .. " " ..
minetest.colorize(desel.colour, desel.nick) .. " " .. desel.message
end
end
num_lines = num_lines + 1
end
for i=increment, #chat do
-- Fixes empty lines being sent to chat
if chat[i] == "" then
else
minetest.chat_send_all(chat[i])
end
end
increment = #chat + 1
end
minetest.after(1, get_discord_messages)
end
minetest.after(2, get_discord_messages)

BIN
lib/libcurl.exp Normal file

Binary file not shown.

BIN
lib/libcurl.lib Normal file

Binary file not shown.

128
main.go Normal file
View File

@ -0,0 +1,128 @@
package main
import (
"bufio"
"fmt"
"os"
"os/signal"
"strconv"
"strings"
"syscall"
"github.com/bwmarrin/discordgo"
)
// Token for both auth
var Token string = ""
// Channel ID
var Channel string = ""
// Server ID
var Server string = ""
func main() {
line := 1
file, err := os.Open("auth.txt")
if err != nil {
_, err = os.OpenFile("auth.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
fmt.Println("Failed to load auth.txt, a file named auth.txt has been created.\nThe file should contain the following lines:\n\n<Discord bot token>\n<Discord text channel ID>\n<Discord server ID>")
return
}
fscanner := bufio.NewScanner(file)
for fscanner.Scan() {
switch line {
case 1:
Token = fscanner.Text()
case 2:
Channel = fscanner.Text()
case 3:
Server = fscanner.Text()
}
line = line + 1
}
// Create a new Discord session using the provided bot token.
dg, err := discordgo.New("Bot " + Token)
if err != nil {
fmt.Println("Error using provided auth token.", err)
return
}
dg.StateEnabled = true
// Add a handler that manages reading of incoming messages.
dg.AddHandler(messageCreate)
// Open a websocket connection to Discord and begin listening.
err = dg.Open()
if err != nil {
fmt.Println("Failed to open websocket to Discord.,", err)
return
}
// Wait here until CTRL-C or other term signal is received.
fmt.Println("Bot is now running. Press CTRL-C to exit. \nUse GNU screen or tmux to keep the heartbeat alive; \notherwise the heartbeat may randomly terminate.")
sc := make(chan os.Signal, 1)
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill)
<-sc
// Cleanly close down the Discord session.
dg.Close()
}
func getRoleColour(userID string, s *discordgo.Session) int {
member, _ := s.GuildMember(Server, userID)
for _, roleID := range member.Roles {
role, err := s.State.Role(Server, roleID)
if err == nil {
return role.Color
}
}
return int(16777215)
}
func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
// Ignore all messages created by the bot itself
if m.Author.ID == s.State.User.ID {
return
}
if m.ChannelID == Channel {
member, err := s.State.Member(Server, m.Author.ID)
userColour := "#ffffff"
member, err = s.GuildMember(Server, m.Author.ID)
roleColour := getRoleColour(m.Author.ID, s)
if roleColour != 0 {
blue := roleColour & 0xFF
green := (roleColour >> 8) & 0xFF
red := (roleColour >> 16) & 0xFF
userColour = "#" + strconv.FormatInt(int64(red), 16) + strconv.FormatInt(int64(green), 16) + strconv.FormatInt(int64(blue), 16)
}
serverNick := m.Author.Username
if member.Nick != "" {
serverNick = member.Nick
}
messageAppend := "return {[\"server\"] = \"[Discord]\", [\"colour\"] = \"" + userColour + "\", [\"nick\"] = \"<" + serverNick + ">\", [\"message\"] = \"" + strings.ReplaceAll(m.Content, "\n", " ") + "\"}\n"
if m.Message.Attachments != nil {
for k := range m.Message.Attachments {
messageAppend = messageAppend + "return {[\"server\"] = \"[Discord]\", [\"colour\"] = \"" + userColour + "\", [\"nick\"] = \"<" + serverNick + ">\", [\"message\"] = \"" + m.Message.Attachments[k].URL + "\"}\n"
}
}
// Lua may randomly delete this file so always attempt to re-create it.
f, err := os.OpenFile("discord.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return
}
f.WriteString(messageAppend)
f.Close()
}
}