├── .gitignore ├── README.md ├── LICENSE └── vbot.v /.gitignore: -------------------------------------------------------------------------------- 1 | vbot 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # discord-bot 2 | 3 | Source code for VBot#9304 on The V Language & Apps Discord server. 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 The V Programming Language 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /vbot.v: -------------------------------------------------------------------------------- 1 | import terisback.discordv as discord 2 | import net.http 3 | import os 4 | import x.json2 5 | import strings 6 | import arrays 7 | import json 8 | import regex 9 | 10 | const ( 11 | bot_token = os.getenv('VBOT_TOKEN') 12 | authorized_roles = os.getenv('AUTHORIZED_ROLES').split(' ') 13 | vexeroot = @VEXEROOT 14 | block_size = 4096 15 | inode_ratio = 16384 16 | ) 17 | 18 | struct State { 19 | headers []string 20 | } 21 | 22 | fn main() { 23 | os.execute('isolate --cleanup') 24 | 25 | mut client := discord.new(token: bot_token) ? 26 | client.userdata = &State{load_docs_headers()} 27 | client.on_message_create(on_message) 28 | client.on_interaction_create(on_interaction) 29 | client.run().wait() 30 | } 31 | 32 | fn reply_to_interaction(data string, id string, token string) { 33 | url := 'https://discord.com/api/v8/interactions/$id/$token/callback' 34 | json := '{"type": 4, "data": $data}' 35 | http.post_json(url, json) or {} 36 | } 37 | 38 | fn on_interaction(mut client discord.Client, interaction &discord.Interaction) { 39 | options := interaction.data.options.map([it.name, it.value]) 40 | 41 | match interaction.data.name { 42 | 'vlib' { 43 | resp := vlib_command(options) 44 | reply_to_interaction(resp, interaction.id, interaction.token) 45 | } 46 | 'docs' { 47 | state := &State(client.userdata) 48 | resp := docs_command(options, state.headers) 49 | reply_to_interaction(resp, interaction.id, interaction.token) 50 | } 51 | else {} 52 | } 53 | } 54 | 55 | fn sanitize(argument string) ? { 56 | for letter in argument { 57 | match letter { 58 | `0`...`9`, `a`...`z`, `A`...`Z`, `.`, `_` {} 59 | else { return error('Illegal character.') } 60 | } 61 | } 62 | } 63 | 64 | struct Section { 65 | name string 66 | content string 67 | comments []string 68 | } 69 | 70 | fn vlib_command(options [][]string) string { 71 | vlib_module := options[0][1] 72 | query := options[1][1] 73 | 74 | sanitize(vlib_module) or { 75 | return '{"content": "Only letters, numbers, ., and _ are allowed in module names."}' 76 | } 77 | 78 | sanitize(query) or { 79 | return '{"content": "Only letters, numbers, ., and _ are allowed in queries."}' 80 | } 81 | 82 | result := os.execute('v doc -f json -o stdout $vlib_module') 83 | 84 | if result.exit_code != 0 { 85 | return '{"content": "Module `$vlib_module` not found."}' 86 | } 87 | 88 | json := json2.raw_decode(result.output) or { 89 | return '{"content": "Decoding `v doc` json failed."}' 90 | } 91 | 92 | mut lowest, mut closest := 2147483647, Section{} 93 | sections := json.as_map()['contents'].arr() 94 | 95 | for section_ in sections { 96 | section := section_.as_map() 97 | name := section['name'].str() 98 | score := strings.levenshtein_distance(query, name) 99 | 100 | if score < lowest { 101 | lowest = score 102 | closest = Section{ 103 | name: name 104 | content: section['content'].json_str() 105 | comments: section['comments'].arr().map(it.as_map()['text'].json_str()) 106 | } 107 | } 108 | 109 | for child_ in section['children'].arr() { 110 | child := child_.as_map() 111 | child_name := child['name'].str() 112 | child_score := strings.levenshtein_distance(query, child_name) 113 | 114 | if child_score < lowest { 115 | lowest = child_score 116 | closest = Section{ 117 | name: child_name 118 | content: child['content'].json_str() 119 | comments: child['comments'].arr().map(it.as_map()['text'].json_str()) 120 | } 121 | } 122 | } 123 | } 124 | 125 | mut description := '```v\\n$closest.content```' 126 | mut blob := '' 127 | 128 | for comment in closest.comments { 129 | blob += comment.trim_left('\u0001') 130 | } 131 | 132 | if blob != '' { 133 | description += '\\n>>> $blob' 134 | } 135 | 136 | return '{ 137 | "embeds": [ 138 | { 139 | "title": "$vlib_module $closest.name", 140 | "description": "$description", 141 | "url": "https://modules.vlang.io/${vlib_module}.html#$closest.name", 142 | "color": 4360181 143 | } 144 | ] 145 | }' 146 | } 147 | 148 | fn load_docs_headers() []string { 149 | mut headers := []string{} 150 | 151 | content := os.read_file('$vexeroot/doc/docs.md') or { panic(err) } 152 | 153 | for line in content.split_into_lines() { 154 | stripped := line.trim_space() 155 | 156 | if stripped.starts_with('* [') { 157 | header := stripped[(stripped.index_byte(`(`) + 1)..(stripped.len - 1)] 158 | 159 | if header[0] == `#` { 160 | headers << header 161 | } 162 | } 163 | } 164 | 165 | return headers 166 | } 167 | 168 | fn docs_command(options [][]string, headers []string) string { 169 | query := '#' + options[0][1] 170 | scores := headers.map(strings.levenshtein_distance(query, it)) 171 | lowest := arrays.min(scores) 172 | header := headers[scores.index(lowest)] 173 | 174 | return '{"content": ""}' 175 | } 176 | 177 | fn on_message(mut client discord.Client, message &discord.Message) { 178 | content := message.content 179 | mut re := regex.regex_opt(r'/run\s+```[a-z]*\s+(.*)```') or { panic(err.msg) } 180 | start, _ := re.match_string(content) 181 | 182 | if start != -1 { 183 | roles := message.member.roles 184 | mut authorized := false 185 | 186 | for role in authorized_roles { 187 | if role in roles { 188 | authorized = true 189 | } 190 | } 191 | 192 | if !authorized { 193 | client.channel_message_send(message.channel_id, 194 | content: "You aren't authorized to use eval." 195 | ) or {} 196 | return 197 | } 198 | 199 | group := re.get_group_list()[0] 200 | code := content[group.start..group.end] 201 | resp := '```\n${run_in_sandbox(code)}\n```' 202 | client.channel_message_send(message.channel_id, content: resp) or {} 203 | } 204 | } 205 | 206 | fn run_in_sandbox(code string) string { 207 | iso_res := os.execute('isolate --init') 208 | defer { 209 | os.execute('isolate --cleanup') 210 | } 211 | box_path := os.join_path(iso_res.output.trim_suffix('\n'), 'box') 212 | os.write_file(os.join_path(box_path, 'code.v'), code) or { 213 | return 'Failed to write code to sandbox.' 214 | } 215 | run_res := os.execute('isolate --dir=$vexeroot --env=HOME=/box --processes=3 --mem=100000 --wall-time=5 --quota=${1048576 / block_size},${1048576 / inode_ratio} --run $vexeroot/v run code.v') 216 | return prettify(run_res.output) 217 | } 218 | 219 | fn prettify(output string) string { 220 | mut pretty := output 221 | 222 | if pretty.len > 1992 { 223 | pretty = pretty[..1989] + '...' 224 | } 225 | 226 | nlines := pretty.count('\n') 227 | if nlines > 5 { 228 | pretty = pretty.split_into_lines()[..5].join_lines() + '\n...and ${nlines - 5} more' 229 | } 230 | 231 | return pretty 232 | } 233 | --------------------------------------------------------------------------------