├── .eslintrc.yml ├── .gitignore ├── LICENSE ├── README.md ├── audio.js ├── away_mover.js ├── cleverbot.js ├── cover.js ├── custom_commands.js ├── discord_moderation.js ├── discord_rename_everyone.js ├── discord_sinusbot.js ├── exec.js ├── group_list.js ├── jsconfig.json ├── package.json ├── stream2icecast.js ├── telegram_bot.js └── uptimerobot.js /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | browser: true 3 | commonjs: true 4 | es6: true 5 | extends: 'eslint:recommended' 6 | globals: 7 | registerPlugin: readonly 8 | parserOptions: 9 | ecmaVersion: 2018 10 | rules: {} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .vscode 3 | 4 | # ignore built-in scripts 5 | command.js 6 | advertising.js 7 | alonemode.js 8 | bookmark.js 9 | followme.js 10 | norecording.js 11 | rememberChannel.js 12 | welcome.js 13 | sinusbot-commands.js 14 | 15 | # ignore test stuff 16 | dev.js 17 | dev.js.disabled 18 | 19 | playlist-manager/ 20 | playlist-manager.js 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Jonas Bögle 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sinusbot scripts 2 | 3 | **THIS IS NO LONGER MAINTAINED** and might be archived soon. 4 | Feel free to fork and work on these scripts on your own (as long as you follow the [MIT license](https://github.com/irgendwr/sinusbot-scripts/blob/master/LICENSE) and mention me). 5 | 6 | This repository contains a few scripts that I wrote for the [SinusBot](https://sinusbot.com). 7 | 8 | ## Installation 9 | 10 | 1. Download the script and put it in the scripts folder where the SinusBot is installed 11 | 2. Restart the SinusBot 12 | 3. Go to your web-interface: Settings -> Scrips and enable the script by checking the box next to it 13 | 4. Configure the script as you like (by clicking on the arrow to show the options and pressing 'save' at the end) 14 | 5. Click on Save changes at the bottom of the page 15 | 16 | ## Scripts 17 | 18 | ### [Custom commands](custom_commands.js) 19 | 20 | This is a simple script that allows you to create your own commands with custom responses. 21 | 22 | See [forum thread](https://forum.sinusbot.com/resources/custom-commands.226/) for more information/discussion. 23 | 24 | **Config:** 25 | 26 | The the following placeholders can be used in the config: 27 | 28 | - username 29 | - mention 30 | - uid 31 | - dbid 32 | - description 33 | - ping 34 | - total_connections 35 | - packetloss 36 | - bytes_sent 37 | - bytes_received 38 | - ip 39 | - first_join 40 | - os 41 | - version 42 | - clients_count 43 | - clients 44 | - channels_count 45 | - channels 46 | - client_groups_count 47 | - client_groups 48 | - server_groups_count 49 | - server_groups 50 | - playing 51 | - random(, ) 52 | - randomString(option one, option two, ...) 53 | - channel_name 54 | - channel_id 55 | - channel_url 56 | - client_channel_name 57 | - client_channel_id 58 | - client_channel_url 59 | 60 | Example: 61 | 62 | ``` 63 | Hi {mention}, your ID is: {uid} and you have the following {client_groups_count} groups: {client_groups}. 64 | Random number between 0 and 100: {random(1, 100)}. 65 | ``` 66 | 67 | ### [Join/Leave](join_leave.js) 68 | 69 | This script adds the commands !join and !leave that make the bot join or leave your channel. 70 | 71 | See [forum thread](https://forum.sinusbot.com/resources/join-leave-commands.423/) for more information/discussion. 72 | 73 | ### [AFK mover (Away/Mute/Deaf/Idle)](away_mover.js) 74 | 75 | This script moves clients that are set as away, have their speakers/mic muted or are idle to a specified channel. 76 | 77 | See [forum thread](https://forum.sinusbot.com/resources/away-mover.179/) for more information/discussion. 78 | 79 | ### [Uptimerobot - Server Status/Uptime Monitoring](uptimerobot.js) 80 | 81 | Informs you about the status of a server configured on [uptimerobot.com](https://uptimerobot.com) 82 | 83 | See [forum thread](https://forum.sinusbot.com/resources/uptimerobot.127/) for more information/discussion. 84 | 85 | **Config:** 86 | 87 | In the config the following placeholders can be used: 88 | 89 | - %name% 90 | - %uptime% 91 | - %url% 92 | - %port% 93 | - %type% 94 | - %status% 95 | - %id% 96 | - %created% 97 | - %ssl.brand% 98 | - %ssl.product% 99 | - %ssl.expires% 100 | 101 | ### [Group List](group_list.js) 102 | 103 | List the servers groups and their IDs with the `!groups` command. 104 | 105 | See [forum thread](https://forum.sinusbot.com/resources/group-list.388/) for more information/discussion. 106 | 107 | ## Troubleshooting 108 | 109 | - make sure that you have the latest version of the SinusBot (some scripts require at least version 1.0.0) 110 | - make sure that you have the latest version of this script 111 | - read the instructions above carefully and check if you've missed anything 112 | - If you've checked everything and it still doesn't work then you can ask for help in the discussion thread or open an issue on GitHub. 113 | But hold on for a second! Before you post: [read this first](https://forum.sinusbot.com/threads/read-me-before-you-post.342/) and include all of the required information. 114 | -------------------------------------------------------------------------------- /audio.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Forum: 3 | * GitHub: https://github.com/irgendwr/sinusbot-scripts 4 | */ 5 | 6 | registerPlugin({ 7 | name: 'Audio Filter', 8 | version: '1.0.0', 9 | description: 'Set audio filters.', 10 | author: 'Jonas Bögle (irgendwr)', 11 | backends: ['ts3', 'discord'], 12 | vars: [ 13 | { 14 | title: 'Preset', 15 | name: 'preset', 16 | type: 'select', 17 | options: [ 18 | 'off (default)', 19 | 'slightly compress', 20 | 'heavy compress (FM Radio)' 21 | ], 22 | default: '0' 23 | }, 24 | { 25 | title: 'Playback Speed', 26 | name: 'speed', 27 | type: 'select', 28 | options: [ 29 | 'normal (default)', 30 | 'slow (0.75)', 31 | 'fast (1.25)', 32 | 'faster (1.5)', 33 | 'fasterer (2.0)' 34 | ], 35 | default: '0' 36 | } 37 | ] 38 | }, (_, {speed, preset}, meta) => { 39 | const engine = require('engine') 40 | const audio = require('audio') 41 | 42 | engine.log(`Loaded ${meta.name} v${meta.version} by ${meta.author}.`) 43 | 44 | var fs = [] 45 | 46 | switch (speed) { 47 | case '1': 48 | fs.push('atempo=0.75') 49 | break 50 | case '2': 51 | fs.push('atempo=1.25') 52 | break 53 | case '3': 54 | fs.push('atempo=1.5') 55 | break 56 | case '4': 57 | fs.push('atempo=2.0') 58 | break 59 | } 60 | 61 | switch (preset) { 62 | case '1': 63 | fs.push('compand=.3|.3:1|1:-90/-60|-60/-40|-40/-30|-20/-20:6:0:-90:0.2') 64 | break 65 | case '2': 66 | fs.push('volume=-3dB,asplit=5[in1][in2][in3][in4][in5];[in1]lowpass=f=100:p=1,compand=.005|.005:.1|.1:-47/-40|-34/-34|-17/-33[out1];[in2]highpass=f=100:p=1,lowpass=f=400:p=1,compand=.003|.003:.05|.05:-47/-40|-34/-34|-17/-33[out2];[in3]highpass=f=400:p=1,lowpass=f=1600:p=1,compand=.000625|.000625:.0125|.0125:-47/-40|-34/-34|-15/-33[out3];[in4]highpass=f=1600:p=1,lowpass=f=6400:p=1,compand=.0001|.0001:.025|.025:-47/-40|-34/-34|-31/-31|-0/-30[out4];[in5]highpass=f=6400:p=1,compand=.0|.0:.025|.025:-38/-31|-28/-28|-0/-25[out5];[out1][out2][out3][out4][out5]amix=inputs=5,volume=+27dB,volume=-12dB,lowpass=f=17500:p=1,volume=+12dB,volume=-3dB,lowpass=f=17801:p=1') 67 | break 68 | } 69 | 70 | if (fs.length === 0) { 71 | audio.setAudioFilter('') 72 | } else { 73 | audio.setAudioFilter('[a]'+fs.join(',')+'[out]') 74 | } 75 | }) -------------------------------------------------------------------------------- /away_mover.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Forum: https://forum.sinusbot.com/resources/away-mover.179/ 3 | * GitHub: https://github.com/irgendwr/sinusbot-scripts 4 | */ 5 | 6 | registerPlugin({ 7 | name: 'AFK mover (Away/Mute/Deaf/Idle)', 8 | version: '2.4.0', 9 | description: 'Moves clients that are set as away, have their speakers/mic muted or are idle to a specified channel', 10 | author: 'Jonas Bögle (irgendwr)', 11 | vars: [ 12 | /*** general ***/ 13 | { 14 | name: 'afkChannel', 15 | title: 'AFK Channel', 16 | type: 'channel' 17 | }, 18 | 19 | /*** away ***/ 20 | { 21 | name: 'awayEnabled', 22 | title: '[AWAY] Move users who set themselves as away', 23 | type: 'checkbox' 24 | }, 25 | { 26 | name: 'awayMoveBack', 27 | title: 'Move users back', 28 | type: 'checkbox', 29 | indent: 3, 30 | conditions: [{ field: 'awayEnabled', value: true }] 31 | }, 32 | { 33 | name: 'awaySgBlacklist', 34 | title: 'Ignore users with these servergroups:', 35 | type: 'array', 36 | vars: [{ 37 | name: 'servergroup', 38 | title: 'Servergroup', 39 | type: 'number' 40 | }], 41 | indent: 3, 42 | conditions: [{ field: 'awayEnabled', value: true }] 43 | }, 44 | { 45 | name: 'awayChannelIgnoreType', 46 | title: 'Should this only apply to certain channels?', 47 | type: 'select', 48 | options: [ 49 | 'No, check every channel.', 50 | 'Yes, whitelist the channels below.', 51 | 'Yes, blacklist the channels below.' 52 | ], 53 | indent: 3, 54 | conditions: [{ field: 'awayEnabled', value: true }] 55 | }, 56 | { 57 | name: 'awayChannelList', 58 | title: 'Channels:', 59 | type: 'array', 60 | vars: [{ 61 | name: 'channel', 62 | title: 'Channel', 63 | type: 'channel' 64 | }], 65 | indent: 3, 66 | conditions: [{ field: 'awayEnabled', value: true }] 67 | }, 68 | { 69 | name: 'awayDelay', 70 | title: 'Delay (in seconds)', 71 | type: 'number', 72 | indent: 3, 73 | conditions: [{ field: 'awayEnabled', value: true }] 74 | }, 75 | 76 | /*** mute ***/ 77 | { 78 | name: 'muteEnabled', 79 | title: '[MUTE] Move users who mute themselves', 80 | type: 'checkbox' 81 | }, 82 | { 83 | name: 'muteMoveBack', 84 | title: 'Move users back', 85 | type: 'checkbox', 86 | indent: 3, 87 | conditions: [{ field: 'muteEnabled', value: true }] 88 | }, 89 | { 90 | name: 'muteSgBlacklist', 91 | title: 'Ignore users with these servergroups:', 92 | type: 'array', 93 | vars: [{ 94 | name: 'servergroup', 95 | title: 'Servergroup', 96 | type: 'number' 97 | }], 98 | indent: 3, 99 | conditions: [{ field: 'muteEnabled', value: true }] 100 | }, 101 | { 102 | name: 'muteChannelIgnoreType', 103 | title: 'Should this only apply to certain channels?', 104 | type: 'select', 105 | options: [ 106 | 'No, check every channel.', 107 | 'Yes, whitelist the channels below.', 108 | 'Yes, blacklist the channels below.' 109 | ], 110 | indent: 3, 111 | conditions: [{ field: 'muteEnabled', value: true }] 112 | }, 113 | { 114 | name: 'muteChannelList', 115 | title: 'Channels:', 116 | type: 'array', 117 | vars: [{ 118 | name: 'channel', 119 | title: 'Channel', 120 | type: 'channel' 121 | }], 122 | indent: 3, 123 | conditions: [{ field: 'muteEnabled', value: true }] 124 | }, 125 | { 126 | name: 'muteDelay', 127 | title: 'Delay (in seconds)', 128 | type: 'number', 129 | indent: 3, 130 | conditions: [{ field: 'muteEnabled', value: true }] 131 | }, 132 | 133 | /*** deaf ***/ 134 | { 135 | name: 'deafEnabled', 136 | title: '[DEAF] Move users who deactivate their speaker', 137 | type: 'checkbox' 138 | }, 139 | { 140 | name: 'deafMoveBack', 141 | title: 'Move users back', 142 | type: 'checkbox', 143 | indent: 3, 144 | conditions: [{ field: 'deafEnabled', value: true }] 145 | }, 146 | { 147 | name: 'deafSgBlacklist', 148 | title: 'Ignore users with these servergroups:', 149 | type: 'array', 150 | vars: [{ 151 | name: 'servergroup', 152 | title: 'Servergroup', 153 | type: 'number' 154 | }], 155 | indent: 3, 156 | conditions: [{ field: 'deafEnabled', value: true }] 157 | }, 158 | { 159 | name: 'deafChannelIgnoreType', 160 | title: 'Should this only apply to certain channels?', 161 | type: 'select', 162 | options: [ 163 | 'No, check every channel.', 164 | 'Yes, whitelist the channels below.', 165 | 'Yes, blacklist the channels below.' 166 | ], 167 | indent: 3, 168 | conditions: [{ field: 'deafEnabled', value: true }] 169 | }, 170 | { 171 | name: 'deafChannelList', 172 | title: 'Channels:', 173 | type: 'array', 174 | vars: [{ 175 | name: 'channel', 176 | title: 'Channel', 177 | type: 'channel' 178 | }], 179 | indent: 3, 180 | conditions: [{ field: 'deafEnabled', value: true }] 181 | }, 182 | { 183 | name: 'deafDelay', 184 | title: 'Delay (in seconds)', 185 | type: 'number', 186 | indent: 3, 187 | conditions: [{ field: 'deafEnabled', value: true }] 188 | }, 189 | 190 | /*** idle ***/ 191 | { 192 | name: 'idleEnabled', 193 | title: '[IDLE] Move users who are idle for too long', 194 | type: 'checkbox' 195 | }, 196 | { 197 | name: 'idleSgBlacklist', 198 | title: 'Ignore users with these servergroups:', 199 | type: 'array', 200 | vars: [{ 201 | name: 'servergroup', 202 | title: 'Servergroup', 203 | type: 'number' 204 | }], 205 | indent: 3, 206 | conditions: [{ field: 'idleEnabled', value: true }] 207 | }, 208 | { 209 | name: 'idleChannelIgnoreType', 210 | title: 'Should this only apply to certain channels?', 211 | type: 'select', 212 | options: [ 213 | 'No, check every channel.', 214 | 'Yes, whitelist the channels below.', 215 | 'Yes, blacklist the channels below.' 216 | ], 217 | indent: 3, 218 | conditions: [{ field: 'idleEnabled', value: true }] 219 | }, 220 | { 221 | name: 'idleChannelList', 222 | title: 'Channels:', 223 | type: 'array', 224 | vars: [{ 225 | name: 'channel', 226 | title: 'Channel', 227 | type: 'channel' 228 | }], 229 | indent: 3, 230 | conditions: [{ field: 'idleEnabled', value: true }] 231 | }, 232 | { 233 | name: 'idleThreshold', 234 | title: 'How long are people allowed to be idle? (in minutes, don\'t use small values!)', 235 | type: 'number', 236 | indent: 3, 237 | conditions: [{ field: 'idleEnabled', value: true }] 238 | }, 239 | 240 | /*** general - notify ***/ 241 | { 242 | name: 'notifyEnabled', 243 | title: 'Notify users when they get moved', 244 | type: 'checkbox' 245 | }, 246 | { 247 | name: 'notifyType', 248 | title: 'How should users be notified?', 249 | type: 'select', 250 | options: [ 'chat', 'poke' ], 251 | indent: 3, 252 | conditions: [{ field: 'notifyEnabled', value: true }] 253 | }, 254 | { 255 | name: 'notifyText', 256 | title: 'Notification message (%reason% = away/mute/deaf/idle)', 257 | type: 'string', 258 | placeholder: 'You were moved to the afk channel, reason: %reason%', 259 | indent: 3, 260 | conditions: [{ field: 'notifyEnabled', value: true }] 261 | }, 262 | ] 263 | }, function (sinusbot, config, info) { 264 | 265 | // include modules 266 | var event = require('event') 267 | var engine = require('engine') 268 | var backend = require('backend') 269 | 270 | // set default config values 271 | config.awayMoveBack = config.awayMoveBack || false 272 | config.awayDelay = config.awayDelay || 0 273 | config.muteMoveBack = config.muteMoveBack || false 274 | config.muteDelay = config.muteDelay || 0 275 | config.deafMoveBack = config.deafMoveBack || false 276 | config.deafDelay = config.deafDelay || 0 277 | config.idleThreshold = config.idleThreshold || 0 278 | config.notifyEnabled = config.notifyEnabled || false 279 | config.notifyType = config.notifyType || 0 280 | config.notifyText = config.notifyText && config.notifyText != '' ? config.notifyText : 'You were moved to the afk channel, reason: %reason%' 281 | engine.saveConfig(config) 282 | 283 | var log = new Logger() 284 | log.debug = false 285 | var idleThreshold = config.idleThreshold * 60 * 1000 286 | var afkChannel = backend.getChannelByID(config.afkChannel) 287 | var afk = [] 288 | var queue = [] 289 | var lastMoveEvent = {} 290 | 291 | // check whether afk channel is set 292 | if (!config.afkChannel) { 293 | engine.notify('You need to specify an afk channel in the config.') 294 | log.e('You need to specify an afk channel in the config.') 295 | return 296 | } 297 | 298 | // check whether afk channel is valid 299 | if (!afkChannel) { 300 | log.w('Unable to find afk channel.') 301 | } 302 | 303 | event.on('connect', function() { 304 | var channel = backend.getChannelByID(config.afkChannel) 305 | if (channel) { 306 | afkChannel = channel 307 | } else { 308 | log.w('AFK Channel not found on connect.') 309 | } 310 | }) 311 | 312 | event.on('load', function() { 313 | var channel = backend.getChannelByID(config.afkChannel) 314 | if (channel) { 315 | afkChannel = channel 316 | } else { 317 | log.w('AFK Channel not found on load.') 318 | } 319 | }) 320 | 321 | // log info on startup 322 | log.i('debug messages are ' + (log.debug ? 'en' : 'dis') + 'abled') 323 | log.i(info.name + ' v' + info.version + ' by ' + info.author + ' loaded successfully.') 324 | 325 | var events = [ 326 | { 327 | name: 'away', 328 | afk: 'clientAway', 329 | back: 'clientBack' 330 | }, 331 | { 332 | name: 'mute', 333 | afk: 'clientMute', 334 | back: 'clientUnmute' 335 | }, 336 | { 337 | name: 'deaf', 338 | afk: 'clientDeaf', 339 | back: 'clientUndeaf' 340 | }, 341 | ] 342 | events.forEach(function (ev) { 343 | if (config[ev.name + 'Enabled']) { 344 | log.d(ev.name + ' is enabled') 345 | 346 | event.on(ev.afk, function (client) { 347 | log.d(ev.afk + ': ' + client.nick()) 348 | 349 | if (groupIsIncluded(client, config[ev.name + 'SgBlacklist']) && channelIsIncluded(client, config[ev.name + 'ChannelIgnoreType'], config[ev.name + 'ChannelList'])) { 350 | if (config[ev.name + 'Delay']) { 351 | addQueue(client, ev.name) 352 | } else { 353 | setAFK(client, ev.name) 354 | } 355 | } else { 356 | log.d('blacklisted, ignoring') 357 | } 358 | }) 359 | 360 | event.on(ev.back, function (client) { 361 | log.d(ev.back + ': ' + client.nick()) 362 | 363 | removeAFK(client, ev.name) 364 | }) 365 | } 366 | }) 367 | 368 | function addQueue(client, event) { 369 | log.d('pushing client to queue (' + event + ')') 370 | 371 | queue.push({ 372 | event: event, 373 | uid: client.uid(), 374 | timestamp: timestamp() 375 | }) 376 | } 377 | 378 | function removeQueue(index) { 379 | queue.splice(index, 1) 380 | } 381 | 382 | function checkQueue() { 383 | queue.forEach(function (queuedAFKclient, i) { 384 | var event = queuedAFKclient.event 385 | var client = backend.getClientByUID(queuedAFKclient.uid) 386 | 387 | if (!client) { 388 | log.d('Error: client not found') 389 | 390 | removeQueue(i) 391 | return 392 | } 393 | 394 | var stillAFK = false 395 | switch (event) { 396 | case 'away': stillAFK = client.isAway(); break 397 | case 'mute': stillAFK = client.isMuted(); break 398 | case 'deaf': stillAFK = client.isDeaf(); break 399 | } 400 | 401 | if (stillAFK) { 402 | var timePassed = timestamp() - queuedAFKclient.timestamp 403 | var delay = config[event + 'Delay'] * 1000 404 | 405 | if (timePassed >= delay) { 406 | setAFK(client, event) 407 | } else { 408 | // don't remove from queue, continue waiting 409 | return 410 | } 411 | } 412 | removeQueue(i) 413 | }) 414 | } 415 | 416 | // check queue every 2s 417 | setInterval(checkQueue, 2 * 1000) 418 | 419 | if (config.idleEnabled) { 420 | log.d('idle move is enabled') 421 | 422 | setInterval(checkIdle, 10 * 1000) 423 | checkIdle() 424 | 425 | // workaround to improve idle time accuracy 426 | event.on('clientMove', function (ev) { 427 | if (ev.toChannel) { 428 | lastMoveEvent[ev.client.uid()] = timestamp() 429 | } else { 430 | // remove client on disconnect 431 | delete lastMoveEvent[ev.client.uid()] 432 | delete afk[ev.client.uid()] 433 | } 434 | }) 435 | } else { 436 | event.on('clientMove', function (ev) { 437 | if (!ev.toChannel) { 438 | // remove client on disconnect 439 | delete afk[ev.client.uid()] 440 | } 441 | }) 442 | } 443 | 444 | function checkIdle() { 445 | backend.getClients().forEach(function (client) { 446 | if (afkChannel.equals(client.getChannels()[0])) { 447 | // client is already in afk channel 448 | return 449 | } 450 | 451 | if (client.getIdleTime() > idleThreshold && (lastMoveEvent[client.uid()] || 0) < timestamp() - idleThreshold) { 452 | if (groupIsIncluded(client, config.idleSgBlacklist) && channelIsIncluded(client, config.idleChannelIgnoreType, config.idleChannelList)) { 453 | setAFK(client, 'idle') 454 | } 455 | } 456 | }) 457 | } 458 | 459 | /** 460 | * @param {Client} client 461 | * @param {string} event away/mute/deaf/idle 462 | */ 463 | function removeAFK(client, event) { 464 | var afkClient = afk[client.uid()] ? afk[client.uid()][event] : null 465 | 466 | if (afkClient) { 467 | var moveBack = config[event + 'MoveBack'] 468 | log.d(client.nick() + ' was away for ' + Math.round((timestamp() - afkClient.timestamp) / 1000) + 's') 469 | log.d('moveBack: ' + moveBack) 470 | 471 | afk[client.uid()][event] = null 472 | 473 | if (moveBack) { 474 | var prevChannel = backend.getChannelByID(afkClient.prevChannel) 475 | log.d('moving client back to prev channel (' + prevChannel.id() + '/' + prevChannel.name() + ')') 476 | 477 | client.moveTo(prevChannel) 478 | } 479 | } else { 480 | log.d('Client ' + client.nick() + ' is not saved as afk') 481 | } 482 | } 483 | 484 | /** 485 | * @param {Client} client 486 | * @param {string} event away/mute/deaf/idle 487 | */ 488 | function setAFK(client, event) { 489 | if (afkChannel.equals(client.getChannels()[0])) { 490 | log.d(client.nick() + ' is already AFK (' + event + ') (' + afkChannel.id() + '==' + client.getChannels()[0].id()) 491 | return 492 | } 493 | 494 | log.d(client.nick() + ' is AFK (' + event + ')') 495 | 496 | if (!afk[client.uid()]) { 497 | // initialize 498 | afk[client.uid()] = {} 499 | } 500 | 501 | afk[client.uid()][event] = { 502 | prevChannel: client.getChannels()[0].id(), 503 | timestamp: timestamp() 504 | } 505 | 506 | client.moveTo(afkChannel) 507 | 508 | if (config.notifyEnabled) { 509 | var msg = config.notifyText.replace(/%reason%/gi, event) 510 | switch (config.notifyType) { 511 | case "0": 512 | case 0: client.chat(msg); break 513 | case "1": 514 | case 1: client.poke(msg); break 515 | } 516 | } 517 | } 518 | 519 | /** 520 | * Checks if a client has a servergroup that is blacklisted 521 | * 522 | * @param {Client} client 523 | * @param {Array} blacklist blacklist config array 524 | * @return {boolean} 525 | */ 526 | function groupIsIncluded(client, blacklist) { 527 | if (!blacklist) 528 | return true 529 | 530 | return !client.getServerGroups().some(function (servergroup) { 531 | return blacklist.some(function (blacklistItem) { 532 | return servergroup.id() == blacklistItem.servergroup 533 | }) 534 | }) 535 | } 536 | 537 | /** 538 | * Checks wheter the channel a client is in should be checked. 539 | * 540 | * @param {Client} client 541 | * @param {Number} listType 542 | * @param {Array} channelList 543 | * @return {boolean} 544 | */ 545 | function channelIsIncluded(client, listType, channelList) { 546 | if (!listType) { 547 | return true 548 | } 549 | 550 | if (!channelList) { 551 | // returns true if blacklist 552 | return listType != 1 553 | } 554 | 555 | // returns true if (whitelist and included) or (not whitelist and not included) 556 | return (listType == 1) == client.getChannels().some(function (channel) { 557 | return channelList.some(function (item) { 558 | return channel.id() == item.channel 559 | }) 560 | }) 561 | } 562 | 563 | /** 564 | * Returns the current timestamp in ms 565 | * 566 | * @return {number} timestamp 567 | */ 568 | function timestamp() { 569 | return Date.now() 570 | } 571 | 572 | /** 573 | * Creates a logging interface 574 | * @requires engine 575 | */ 576 | function Logger() { 577 | this.debug = false 578 | this.log = function (level, msg) { 579 | if (typeof msg == 'object') { 580 | msg = JSON.stringify(msg) 581 | } 582 | engine.log('[' + level + '] ' + msg) 583 | } 584 | this.e = function (msg) { 585 | this.log('ERROR', msg) 586 | } 587 | this.w = function (msg) { 588 | this.log('WARN', msg) 589 | } 590 | this.i = function (msg) { 591 | this.log('INFO', msg) 592 | } 593 | this.d = function (msg) { 594 | if (this.debug) 595 | this.log('DEBUG', msg) 596 | } 597 | } 598 | }) -------------------------------------------------------------------------------- /cleverbot.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Forum: 3 | * GitHub: https://github.com/irgendwr/sinusbot-scripts 4 | */ 5 | 6 | registerPlugin({ 7 | name: 'Cleverbot', 8 | version: '1.0', 9 | description: 'Talk to cleverbot by using the ask command or in a specified channel.', 10 | author: 'Jonas Bögle (irgendwr)', 11 | backends: ['ts3', 'discord'], 12 | engine: '>=1.0.0', 13 | requiredModules: ['http', 'discord-dangerous'], 14 | vars: [ 15 | { 16 | name: 'channel', 17 | title: 'Channel ID', 18 | type: 'string' 19 | }, 20 | { 21 | name: 'minDelay', 22 | title: 'Minimum Delay (in milliseconds)', 23 | default: 1000, 24 | type: 'number' 25 | }, 26 | { 27 | name: 'tts', 28 | title: 'TTS (text to speech)', 29 | default: false, 30 | type: 'checkbox' 31 | } 32 | ] 33 | }, (_, {channel, minDelay, tts}, meta) => { 34 | const http = require('http') 35 | const event = require('event') 36 | const engine = require('engine') 37 | const backend = require('backend') 38 | const helpers = require('helpers') 39 | const audio = require('audio') 40 | 41 | class Cleverbot { 42 | constructor() { 43 | // keeps track of the chat log 44 | this.chat = [] 45 | // session token thing 46 | this.XVIS = null 47 | } 48 | 49 | /** 50 | * Creates a new session. 51 | * @param {(error?: string) => void} [callback] 52 | */ 53 | init(callback) { 54 | let date = new Date() 55 | let month = date.getMonth()+1 56 | let day = date.getDate() 57 | let datestr = ''+date.getFullYear() 58 | if (month < 10) datestr += '0' 59 | datestr += month 60 | if (day < 10) datestr += '0' 61 | datestr += day 62 | 63 | this._send('GET', Cleverbot.initURL+datestr, null, (error, response) => { 64 | if (error) { 65 | engine.log(`HTTP Error: ${error}`) 66 | if (typeof callback === 'function') callback(`HTTP Error: ${error}`) 67 | return 68 | } 69 | 70 | this.XVIS = Cleverbot._getCookie('XVIS', response.headers) 71 | 72 | if (this.XVIS) { 73 | //engine.log(`XVIS: ${this.XVIS}`) 74 | if (typeof callback === 'function') callback() 75 | } else { 76 | engine.log('Error: cookie not found') 77 | if (typeof callback === 'function') callback('Error: cookie not found') 78 | } 79 | }) 80 | } 81 | 82 | /** 83 | * Resets the current state and creates a new session. 84 | * @param {(error?: string) => void} [callback] 85 | */ 86 | reset(callback) { 87 | engine.log('reset') 88 | this.chat = [] 89 | //this.XVIS = null 90 | this.init(callback) 91 | } 92 | 93 | /** 94 | * Ask Cleverbot something. 95 | * @param {string} input 96 | * @param {function} callback 97 | * @param {number} retries How often to retry before giving up. 98 | */ 99 | ask(input, callback, retries=3) { 100 | let len = this.chat.length 101 | 102 | // Check if we have to wait for the bot to write something. 103 | if (len % 2 !== 0) { 104 | engine.log('Ignoring message since it\'s not our turn to speak. Hint: You can reset via the reset-cleverbot command.') 105 | return; 106 | } 107 | 108 | let url = Cleverbot.api 109 | let body = `stimulus=${Cleverbot._encode(input)}` 110 | for (let i = 0; i < len && i < 7; i++) { 111 | body += `&vText${i + 2}=${Cleverbot._encode(this.chat[len - i - 1])}` 112 | } 113 | this.chat.push(input) 114 | 115 | /* 116 | let lang = Cleverbot._guessLanguage(input); 117 | if (lang) { 118 | body += ('&cb_settings_language=' + lang); 119 | } 120 | */ 121 | 122 | body += '&cb_settings_scripting=no'; 123 | if (this.sessionid) { 124 | body += `&sessionid=${this.sessionid}` 125 | } 126 | 127 | body += '&islearning=1&icognoid=wsf&icognocheck='; 128 | body += helpers.MD5Sum(body.substring(7, 33)) 129 | 130 | this._send('POST', url, body, (error, response) => { 131 | if (error) { 132 | this.chat.pop() 133 | engine.log(`HTTP Error: ${error}`) 134 | if (retries <= 0) { 135 | if (typeof callback === 'function') callback(error, null) 136 | return; 137 | } else { 138 | engine.log('retry...') 139 | return this.ask(input, callback, --retries) 140 | } 141 | } 142 | 143 | let res = response.data.toString().split('\r') 144 | let answer = res[0].trim() 145 | 146 | // Catch invalid answers such as: 147 | // 'DENIED' 148 | // '' 149 | if (answer.toLowerCase().startsWith('')) { 150 | this.chat.pop() 151 | engine.log('Error: Request denied by API') 152 | if (typeof callback === 'function') callback('Request denied by API', null) 153 | this.reset() 154 | return; 155 | // The default answer is also a bad sign: 156 | } else if (answer == 'Hello from Cleverbot') { 157 | this.reset(error => { 158 | if (error) { 159 | if (typeof callback === 'function') callback('Invalid response by API', null) 160 | return; 161 | } 162 | engine.log('retry...') 163 | this.ask(input, callback, --retries); 164 | }) 165 | return; 166 | } 167 | this.chat.push(answer) 168 | if (this.chat.length > Cleverbot.chatLen) { 169 | // remove first two 170 | this.chat.splice(0, 2) 171 | } 172 | 173 | if (!this.sessionid) { 174 | this.sessionid = res[1] 175 | engine.log(`sessionid: ${this.sessionid}`) 176 | } 177 | 178 | //engine.log(`answer: ${answer}`) 179 | if (typeof callback === 'function') callback(null, answer) 180 | }) 181 | } 182 | 183 | /** 184 | * @private 185 | * @param {string} method 186 | * @param {string} url 187 | * @param {string} body 188 | * @param {function} callback 189 | */ 190 | _send(method, url, body, callback) { 191 | //engine.log(url) 192 | //engine.log(body) 193 | 194 | let cookies = this.XVIS ? `XVIS=${this.XVIS}` : null 195 | return http.simpleRequest({ 196 | method: method, 197 | url: url, 198 | timeout: Cleverbot.defaultTimeout, 199 | body: body, 200 | headers: { 201 | 'Referer': Cleverbot.base, 202 | 'Cookie': cookies 203 | } 204 | }, callback.bind(this)) 205 | } 206 | 207 | /** 208 | * Encodes a parameter. 209 | * @private 210 | * @static 211 | * @param {string} input 212 | * @returns {string} 213 | */ 214 | static _encode(input) { 215 | let out = '' 216 | input = input.replace(/[|]/g, '{*}') 217 | 218 | for (let i = 0; i <= input.length; i++) { 219 | if (input.charCodeAt(i) > 255) { 220 | let escapedChar = escape(input.charAt(i)) 221 | if (escapedChar.substring(0, 2) == '%u') { 222 | out += ('|' + escapedChar.substring(2, escapedChar.length)) 223 | } else { 224 | out += escapedChar 225 | } 226 | } else { 227 | out += input.charAt(i) 228 | } 229 | } 230 | out = out.replace(/\|201[CD89]|`|%B4/g, '\'').replace(/\|FF20|\|FE6B/g, '') 231 | return escape(out) 232 | } 233 | 234 | /** 235 | * Returns the value of a cookie. 236 | * @private 237 | * @static 238 | * @param {string} name Name 239 | * @param {object} headers HTTP Headers 240 | * @return {?string} 241 | */ 242 | static _getCookie(name, headers) { 243 | let cookies = headers['Set-Cookie'] 244 | 245 | if (!cookies || cookies.length === 0) return null; 246 | 247 | for (let cookie of cookies) { 248 | cookie = cookie.split('=') 249 | 250 | if (cookie[0] == name) { 251 | return cookie[1].split(';')[0] 252 | } 253 | } 254 | return null 255 | } 256 | 257 | // TODO: either improve or remove 258 | // /** 259 | // * Guess language of given input. 260 | // * @private 261 | // * @static 262 | // * @param {string} input Input 263 | // * @return {?string} lang 264 | // */ 265 | // static _guessLanguage(input) { 266 | // const common = { 267 | // de: ["aber", "ach egal", "achso", "aha ich", "alles", "also bist", "also doch", "also wie", "antwort", "auch", "auf", "aus deut", "ausgez", "beantwort", "bei dir", "bei mir", "beides", "bekomm", "beleid", "beschr", "besser", "bestimm", "beweis", "bin ich", "bissch", "bitte", "chatten", "danke", "dann", "darum", "das be", "das bin", "das du", "das freut", "das glaub", "das habe", "das ich", "das ist", "das stimm", "das war", "das weiss", "dein", "deine", "denke", "deswege", "diese", "deutsch", 268 | // "dich", "doch", "ein mann", "ein mensch", "eine frau", "einfach", "entschuld", "erzahl", "es ist", "falsch", "find ich", "findest", "frau", "fresse", "freund", "freut", "ganz", "gar nicht", "geht", "gehst", "gibt", "gibst", "gib mir", "glaub", "gute", "gut und", "genau", "habe", "haben", "hast", "heiss", "heute", "ich auch", "ich bin", "ich hab", "ich hei", "ich wol", "immer", "junge", "kann", "kannst", "kein", "keine", "klar", "liebst", "madchen", "magst", "meine", "mir ist", "nein", 269 | // "nicht", "sagst", "sagte", "schon", "sprech", "sprichst", "tust", "und", "viel", "warum", "was ist", "was mach", "weil", "wenn", "wer ist", "wieder", "wieso", "willst", "wirklich", "woher", "zum"], 270 | // en: ["hello", "i am", "am I", "you", "you're", "your", "yourself", "i'm", "i'll", "i'd", "can't", "cannot", "don't", "won't", "would", "wouldn't", "could", "couldn't", "you", "is it", "it's", "it is", "isn't", "there", "their", "goodbye", "good", "bye", "what", "what's", "when", "where", "which", "who", "who's", "why", "how", "think", "the", "they", "them", "that", "this", "that's", "very", "favorite", "favourite", "of", "does", "doesn't", "did", "didn't", "yes", "not", "aren't", "never", "every", 271 | // "everything", "anything", "something", "nothing", "thing", "about", "blush", "blushes", "kiss", "kisses", "down", "look", "looks", "more", "even", "around", "into", "get", "got", "love", "i like", "were", "want", "play", "out", "know", "now", "to be", "live", "living", "friend", "friends", "wish", "with", "marry", "wear", "wearing", "doing", "being", "seeing", "smile", "smiles", "gonna", "wanna", "any", "anyway", "sing", "everyone", "everybody", "always", "nope", "maybe", "i do", "really", "indeed", 272 | // "mean", "course", "fine", "well", "sorry", "exactly", "welcome", "because", "sometimes", "tell", "liar", "true", "wrong", "right", "either", "neither", "giggles", "boy", "girl", "agree", "nevermind", "mind", "intelligence", "software", "guess", "interesting", "said", "meaning", "life", "from", "between", "please", "laughs", "talk", "talking", "old", "my name", "understand", "confuse", "confusing", "speak", "speaking", "joke", "awesome", "today", "alright", "sense", "explain", "need", "have", 273 | // "haven't", "make", "makes", "ask", "asking", "question", "english", "is he", "she", "meet", "lies", "probably", "much", "dunno", "ahead", "boyfriend", "girlfriend", "story", "obviously", "first", "correct"], 274 | // es: ["conoces", "crees", "cuando", "donde", "eres", "hablo", "hablas", "pues", "quien", "quiero", "quieres", "sabes", "seguro", "sobre", "aburrido", "acabas", "ademas", "ah vale", "ahora", "alegro", "alguien", "ayer", "bien", "bonito", "comiendo", "como", "conoces", "contigo", "cuando", "cuanto", "dame un", "de nada", "dije", "dijiste", "dimelo", "donde", "encanta", "entonces", "eres", "estamos", "estas", "estoy", "gracias", "gusta", "hablamos", "hacemos", "hola", "hombre", "igual", "interesante", 275 | // "llamas", "llamo", "maquina", "me alegro", "me caes", "mentira", "mi casa", "puedes", "puedo", "pues", "que bueno", "que haces", "que hora", "que pasa", "que tal", "quien", "quieres", "sabes", "seguro", "tambien", "tampoco", "tengo", "tienes", "tu casa", "vamos", "verdad", "vives", "yo soy", "beso", "no se", "espanol", "espa\u00c3\u00b1ol"], 276 | // fr: ["oui", "bien", "bien sur", "d'accord", "j'ai", "au revoir", "toi", "moi", "suis", "je ne", "un peu", "connais", "aimes", "savais", "veux", "voudrais", "ca va", "aussi", "pourquoi", "qu'est", "mais", "bonne", "interessant", "francais", "fran\u00c3\u00a7ais", "comprends", "parce que", "bonjour", "combien", "garcon", "fille", "deja", "voila", "desole", "n'est", "m'aime", "m'appelle", "t'aime", "depuis", "toujours", "quelle", "as-tu", "contraire", "revoir", "aucun", "avec", "vous", "alors", 277 | // "bah alors", "bah c'est", "bah je", "maintenant", "moi non", "bah oui", "bah non", "beaucoup", "laisse", "dites", "c'est bien", "amusant", "dommage", "c'est faux", "c'est gentil", "c'est pas", "c'est un", "car tu", "chacun", "comme", "puis", "t'appelles", "comment tu", "donne", "donnes", "toi tu", "il n'y a", "crois", "vais", "tres drol", "aimez", "quand", "tu as", "mieux", "habites", "j'habite", "voulez", "pouvez", "non plus", "manges", "pensez", "je te", "evidemment", "connait", "que fait", 278 | // "j'avais", "embrasse"], 279 | // it: ["grazie", "questo", "prego", "sicuro", "quando", "come stai", "arrivederchi", "quanti", "tutti", "per favore", "molte", "di niente", "buongiorno", "buona sera", "scusa", "scusi", "bacio", "perche", "chiami", "chiama", "come ti", "anni hai", "femmina", "maschio", "come va", "ti amo", "va bene", "vabbe", "chi sei", "dove sei", "abiti", "sono", "piacere", "anch'io", "anchio", "anche", "quando", "invece", "dimmi", "te lo", "dico", "che", "e come", "cosa fai", "vivi", "nessuno", "nulla", "infatti", 280 | // "quale", "chi e", "certo", "dimmelo", "quindi", "parlo", "italiano", "con te", "allora", "bello", "benissimo", "piu", "capisco", "conosci", "tutto", "quello", "vuoi", "ragione", "credo", "fidanzata", "a mi", "capito", "sei un", "sei una", "andare", "piaccio"], 281 | // nl: ["hoezo", "een", "geen", "uit", "op wie", "hoe is", "niet", "doei", "jawel", "meisje", "waarom", "waar", "nederlands", "mooi", "oud", "ben je", "ik wel", "goed", "jongen", "gaat", "weet", "dankje", "lekker", "woon", "heet", "jij", "leuk", "hou", "hoor", "jou", "tuurlijk", "haat", "daarom", "leuke", "graag", "bedankt", "gewoon", "vind", "schatje", "noem", "klopt", "praten", "praat", "eerst", "mij", "juist", "ik ben", "ben ik"], 282 | // da: ["hvordan", "hej"], 283 | // pl: ["kochasz", "znasz", "polski", "polska", "zemu", "wiem", "masz", "fajnie", "kocham", "dobre", "dobry", "dobrze", "robisz", "dlaczego", "co tam", "imie", "lubisz", "ja tez", "ja nie", "gdzie", "jestem", "jestes", "czego", "bardzo", "ale co", "powiem", "prawda", "mnie", "ciebie", "znaszy", "co to", "jasne", "w domu", "polsce", "lubie", "co nie", "pisze", "pisz", "polsku", "ty tez", "nieprawda", "normalnie", "nie nie", "masz", "gadaj", "nudze", "bo ty", "wiesz", "muw", "cze\u00c5\u203a\u00c4\u2021"], 284 | // pt: ["qual o", "qual e", "quem e", "quem o", "voce e", "voce est", "voce ta", "quer", "quem", "nao", "fala", "falar", "portugues", "portuguesa", "falo port", "tenho", "conhece", "para que", "o que", "meu", "seu", "estou", "isso", "entao", "seu nome", "sim est", "sou", "vou", "tambem", "homem", "mulher", "muito", "bem", "obrigada", "voc\u00c3\u00aa"], 285 | // tr: ["merhaba", "nas\u00c4\u00b1ls\u00c4\u00b1n"] 286 | // } 287 | 288 | // for (let lang in common) { 289 | // for (let str of common[lang]) { 290 | // if (input.includes(str)) { 291 | // //engine.log(`detected lang ${lang} due to string ${str}`) 292 | // return lang 293 | // } 294 | // } 295 | // } 296 | // return null 297 | // } 298 | } 299 | Cleverbot.base = 'https://www.cleverbot.com/' 300 | Cleverbot.initURL = 'https://www.cleverbot.com/extras/conversation-social-min.js?' 301 | Cleverbot.api = Cleverbot.base+'webservicemin?uc=UseOfficialCleverbotAPI&' 302 | Cleverbot.defaultTimeout = 10 * 1000 303 | Cleverbot.chatLen = 10 304 | 305 | const bot = new Cleverbot() 306 | const errResponse = 'Sorry, I was unable to process that due to an API error :confused:' 307 | 308 | engine.log(`Loaded ${meta.name} v${meta.version} by ${meta.author}.`) 309 | 310 | event.on('load', () => { 311 | bot.init() 312 | 313 | const command = require('command') 314 | if (!command) { 315 | engine.log('command.js library not found! Please download command.js to your scripts folder and restart the SinusBot, otherwise this script will not work.'); 316 | engine.log('command.js can be found here: https://github.com/Multivit4min/Sinusbot-Command/blob/master/command.js'); 317 | return; 318 | } 319 | 320 | command.createCommand('ask') 321 | .alias('cleverbot') 322 | .help('Ask something') 323 | .manual('Ask something.') 324 | .addArgument(args => args.rest.setName('message').min(1)) 325 | .exec((client, args, reply, ev) => { 326 | let start = Date.now() 327 | typing(ev.channel.id()) 328 | 329 | bot.ask(args.message, (err, response) => delay(start, () => { 330 | if (err) { 331 | reply(errResponse) 332 | if (engine.getBackend() === 'discord') { 333 | ev.message.createReaction('❌') 334 | } 335 | return; 336 | } 337 | 338 | reply(response) 339 | if (tts) audio.say(response) 340 | })) 341 | }) 342 | 343 | command.createCommand('reset-cleverbot') 344 | .alias('cleverbot') 345 | .help('Reset cleverbot.') 346 | .manual('Reset cleverbot.') 347 | .exec((client, args, reply, ev) => { 348 | engine.log(`Cleverbot reset by: ${client.name()} (${client.uid()})`) 349 | bot.reset() 350 | if (engine.getBackend() === 'discord') { 351 | ev.message.createReaction('✅') 352 | } 353 | }) 354 | }) 355 | 356 | event.on('chat', (/** @type {Message} */ev) => { 357 | if (ev.channel && ev.channel.id().endsWith(channel)) { 358 | // ignore own messages 359 | if (ev.client.isSelf()) return; 360 | 361 | let text = ev.text 362 | // ignore messages starting with "//" 363 | if (text.startsWith('//')) return; 364 | // ignore messages starting with "http(s)://" 365 | if (text.startsWith('https://') || text.startsWith('http://')) return; 366 | 367 | let start = Date.now() 368 | typing(ev.channel.id()) 369 | 370 | bot.ask(text, (err, response) => delay(start, () => { 371 | if (err) { 372 | ev.channel.chat(errResponse) 373 | return; 374 | } 375 | 376 | ev.channel.chat(response) 377 | if (tts) audio.say(response) 378 | })) 379 | } 380 | }) 381 | 382 | /** 383 | * Adds a reaction to a message. 384 | * @param {string} channelID Channel ID 385 | * @param {string} messageID Message ID 386 | * @param {string} emoji Emoji 387 | * @return {Promise} 388 | * @author Jonas Bögle 389 | * @license MIT 390 | */ 391 | function createReaction(channelID, messageID, emoji) { 392 | return discord('PUT', `/channels/${channelID}/messages/${messageID}/reactions/${emoji}/@me`, null, false); 393 | } 394 | 395 | /** 396 | * Post a typing indicator for the specified channel. 397 | * @param {string} channelID 398 | * @author Jonas Bögle 399 | * @license MIT 400 | */ 401 | function typing(channelID) { 402 | if (engine.getBackend() !== 'discord') return; 403 | if (channelID.includes('/')) channelID = channelID.split('/')[1]; 404 | backend.extended().rawCommand('POST', `/channels/${channelID}/typing`, {}, err => { 405 | if (err) { 406 | engine.log(err) 407 | } 408 | }) 409 | } 410 | 411 | /** 412 | * Executes a discord API call 413 | * @param {string} method http method 414 | * @param {string} path path 415 | * @param {object} [data] json data 416 | * @param {boolean} [repsonse] `true` if you're expecting a json response, `false` otherwise 417 | * @return {Promise} 418 | * @author Jonas Bögle 419 | * @license MIT 420 | */ 421 | function discord(method, path, data=null, repsonse=true) { 422 | if (engine.getBackend() !== 'discord') return; 423 | 424 | return new Promise((resolve, reject) => { 425 | backend.extended().rawCommand(method, path, data, (err, data) => { 426 | if (err) return reject(err); 427 | if (repsonse) { 428 | let res; 429 | try { 430 | res = JSON.parse(data); 431 | } catch (err) { 432 | engine.log(`${method} ${path} failed`) 433 | engine.log(`${data}`) 434 | return reject(err); 435 | } 436 | 437 | if (res === undefined) { 438 | engine.log(`${method} ${path} failed`) 439 | engine.log(`${data}`) 440 | return reject('Invalid Response'); 441 | } 442 | 443 | return resolve(res); 444 | } 445 | resolve(); 446 | }); 447 | }); 448 | } 449 | 450 | /** 451 | * Calls a function with a minimum delay. 452 | * @param {number} start timestamp in ms 453 | * @param {function} callback 454 | */ 455 | function delay(start, callback) { 456 | let diff = Date.now() - start 457 | 458 | if (diff >= minDelay) { 459 | callback() 460 | } else { 461 | setTimeout(callback, minDelay-diff) 462 | } 463 | } 464 | }) -------------------------------------------------------------------------------- /cover.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Forum: 3 | * GitHub: https://github.com/irgendwr/sinusbot-scripts 4 | */ 5 | 6 | registerPlugin({ 7 | name: 'Cover', 8 | version: '1.0.0', 9 | description: 'Automatically attempts to download a cover image.', 10 | author: 'Jonas Bögle (irgendwr)', 11 | backends: ['ts3', 'discord'], 12 | requiredModules: ['http'], 13 | vars: [ 14 | { 15 | name: 'provider', 16 | title: 'Data Provider', 17 | type: 'select', 18 | options: [ 19 | 'musicbrainz.org (free, no account required, BUT results are often wrong)', 20 | 'last.fm (free, better, BUT requires account)' 21 | ] 22 | }, { 23 | name: 'lastfm_apikey', 24 | title: 'last.fm API Key (https://www.last.fm/api/account/create)', 25 | type: 'string', 26 | conditions: [{ field: 'provider', value: '1'}] 27 | } 28 | ] 29 | }, (_, config, meta) => { 30 | const event = require('event') 31 | const engine = require('engine') 32 | const http = require('http') 33 | const media = require('media') 34 | 35 | const userAgent = `SinusBot${meta.name}Script/${meta.version} (https://github.com/irgendwr/sinusbot-scripts)` 36 | 37 | engine.log(`Loaded ${meta.name} v${meta.version} by ${meta.author}.`) 38 | 39 | var invalids = []; 40 | 41 | event.on('track', (/** @type {Track} */ track) => { 42 | if (track.thumbnail()) { 43 | engine.log('skipping: track already has a thumbnail') 44 | return; 45 | } 46 | if (invalids.includes(track.id())) { 47 | engine.log('skipping: previously marked as invalid (reload scripts to clear)') 48 | return; 49 | } 50 | if (track.type() !== '' && track.type() !== 'file') { 51 | engine.log('skipping: invalid file type') 52 | return; 53 | } 54 | 55 | switch (config.provider) { 56 | case 1: 57 | case "1": 58 | queryLastFM(track) 59 | break 60 | default: 61 | queryMB(track) 62 | } 63 | }) 64 | 65 | /** 66 | * Queries last.fm for thumbnail 67 | * @param {Track} track 68 | */ 69 | function queryLastFM(track) { 70 | if (!config.lastfm_apikey) { 71 | engine.log('Error: You need to set a last.fm API Key in the Script Settings.') 72 | return; 73 | } 74 | 75 | 76 | let album = track.album() 77 | let artist = track.artist() 78 | let title = track.title() 79 | 80 | if (album && artist) { 81 | engine.log(`Searching last.fm for album thumbnail (${artist} - ${album})...`) 82 | 83 | http.simpleRequest({ 84 | method: 'GET', 85 | url: `http://ws.audioscrobbler.com/2.0/?method=album.getInfo&album=${encodeURIComponent(album)}&artist=${encodeURIComponent(artist)}&api_key=${encodeURIComponent(config.lastfm_apikey)}&format=json`, 86 | timeout: 9000, 87 | headers: { 88 | 'Accept': 'application/json', 89 | 'User-Agent': userAgent 90 | } 91 | }, (error, response) => { 92 | if (error) return engine.log('Error: ' + error) 93 | if (response.statusCode != 200) return engine.log('HTTP Error: ' + response.status) 94 | 95 | // parse JSON response 96 | let result = JSON.parse(response.data.toString()) 97 | 98 | if (result.album && result.album.image.length !== 0) { 99 | // get highest quality image 100 | const thumbnail = result.album.image[Math.min(result.album.image.length, 3)]['#text'] // 3 => "extralarge" 101 | 102 | engine.log('success: '+thumbnail) 103 | track.setThumbnailFromURL(thumbnail) 104 | } else { 105 | engine.log('no results found, marking as invalid') 106 | invalid(track) 107 | } 108 | }) 109 | } else if (title && artist) { 110 | engine.log(`Searching last.fm for track thumbnail (${artist} - ${title})...`) 111 | 112 | http.simpleRequest({ 113 | method: 'GET', 114 | url: `http://ws.audioscrobbler.com/2.0/?method=track.getInfo&track=${encodeURIComponent(title)}&artist=${encodeURIComponent(artist)}&api_key=${encodeURIComponent(config.lastfm_apikey)}&format=json`, 115 | timeout: 9000, 116 | headers: { 117 | 'Accept': 'application/json', 118 | 'User-Agent': userAgent 119 | } 120 | }, (error, response) => { 121 | if (error) return engine.log('Error: ' + error) 122 | if (response.statusCode != 200) return engine.log('HTTP Error: ' + response.status) 123 | 124 | // parse JSON response 125 | let result = JSON.parse(response.data.toString()) 126 | 127 | if (result.track && result.track.album && result.track.album.image.length !== 0) { 128 | // get highest quality image 129 | const thumbnail = result.track.album.image[Math.min(result.track.album.image.length, 3)]['#text'] // 3 => "extralarge" 130 | 131 | engine.log('success: '+thumbnail) 132 | track.setThumbnailFromURL(thumbnail) 133 | } else { 134 | engine.log('no results found, marking as invalid') 135 | invalid(track) 136 | } 137 | }) 138 | } else { 139 | engine.log('not enough info: you need to set artist and (title or album), marking as invalid') 140 | invalid(track) 141 | } 142 | } 143 | 144 | /** 145 | * Queries musicbrainz.org for thumbnail 146 | * @param {Track} track 147 | */ 148 | function queryMB(track) { 149 | let album = track.album() 150 | let artist = track.artist() 151 | let title = track.title() 152 | 153 | if (artist && album) { 154 | engine.log(`Searching MB for release thumbnail (${artist} - ${album})...`) 155 | 156 | http.simpleRequest({ 157 | method: 'GET', 158 | url: `https://musicbrainz.org/ws/2/release/?query="${luceneEscape(album)}"%20AND%20artist:"${luceneEscape(artist)}`, 159 | timeout: 9000, 160 | headers: { 161 | 'Accept': 'application/json', 162 | 'User-Agent': userAgent 163 | } 164 | }, (error, response) => { 165 | if (error) return engine.log('Error: ' + error) 166 | if (response.statusCode != 200) return engine.log('HTTP Error: ' + response.status) 167 | 168 | // parse JSON response 169 | let {releases} = JSON.parse(response.data.toString()) 170 | 171 | if (releases.length !== 0) { 172 | setThumbnailFromMBID(track, releases[0].id) 173 | } else { 174 | engine.log('no results found, marking as invalid') 175 | invalid(track) 176 | } 177 | }) 178 | } else if (title && artist) { 179 | engine.log(`Searching MB for recording thumbnail (${artist} - ${title})...`) 180 | 181 | http.simpleRequest({ 182 | method: 'GET', 183 | url: `https://musicbrainz.org/ws/2/recording/?query="${luceneEscape(title)}"%20AND%20artist:"${luceneEscape(artist)}"%20AND%20dur:[${track.duration()-10000}%20TO%20${track.duration()+10000}]`, 184 | timeout: 9000, 185 | headers: { 186 | 'Accept': 'application/json', 187 | 'User-Agent': userAgent 188 | } 189 | }, (error, response) => { 190 | if (error) return engine.log('Error: ' + error) 191 | if (response.statusCode != 200) return engine.log('HTTP Error: ' + response.status); 192 | 193 | // parse JSON response 194 | let {recordings} = JSON.parse(response.data.toString()) 195 | 196 | if (recordings.length !== 0 && recordings[0].releases.length !== 0) { 197 | setThumbnailFromMBID(track, recordings[0].releases[0].id) 198 | } else { 199 | engine.log('no results found, marking as invalid') 200 | invalid(track) 201 | } 202 | }) 203 | } else { 204 | engine.log('not enough info: you need to set artist and (title or album), marking as invalid') 205 | invalid(track) 206 | } 207 | } 208 | 209 | /** 210 | * Sets a tracks thumbnail. 211 | * @param {Track} track 212 | * @param {string} mbid 213 | */ 214 | function setThumbnailFromMBID(track, mbid) { 215 | //engine.log('mbid: '+mbid) 216 | 217 | http.simpleRequest({ 218 | method: 'GET', 219 | url: `https://coverartarchive.org/release/${mbid}/`, 220 | timeout: 6000, 221 | headers: { 222 | 'Accept': 'application/json', 223 | 'User-Agent': userAgent 224 | } 225 | }, (err, response) => { 226 | if (err) return engine.log('Error: ' + err) 227 | if (response.statusCode != 200) return engine.log('HTTP Error (cover): ' + response.status) 228 | 229 | let {images} = JSON.parse(response.data.toString()) 230 | 231 | if (images && images.length !== 0) { 232 | engine.log('success: '+images[0].thumbnails.large) 233 | track.setThumbnailFromURL(images[0].thumbnails.large) 234 | } else { 235 | engine.log('no image found, marking as invalid') 236 | invalid(track) 237 | } 238 | }); 239 | } 240 | 241 | /** 242 | * Marks a track as invalid 243 | * @param {Track} track 244 | */ 245 | function invalid(track) { 246 | invalids.push(track.id()) 247 | } 248 | 249 | /** 250 | * Escape lucene string 251 | * @see https://lucene.apache.org/core/4_3_0/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#Escaping_Special_Characters 252 | * @param {string} str 253 | * @return {string} 254 | */ 255 | function luceneEscape(str) { 256 | str = str.replace(/[+\-!(){}[\]^"~*?:\\/]|&&|\|\|/gm, "\\$&") 257 | return encodeURIComponent(str) 258 | } 259 | 260 | event.on('load', () => { 261 | const command = require('command') 262 | if (!command) { 263 | engine.log('command.js library not found! Please download command.js to your scripts folder and restart the SinusBot, otherwise this script will not work.'); 264 | engine.log('command.js can be found here: https://github.com/Multivit4min/Sinusbot-Command/blob/master/command.js'); 265 | return; 266 | } 267 | 268 | command.createCommand('setthumbnail') 269 | .help('Set thumbnail of currently playing track') 270 | .manual('Set thumbnail of currently playing track') 271 | .checkPermission(hasEditFilePermission) 272 | // @ts-ignore 273 | .addArgument(command.createArgument('rest').setName('url').min(1)) 274 | .exec((client, args, reply) => { 275 | let track = media.getCurrentTrack() 276 | 277 | if (!track) { 278 | reply('no track playing') 279 | return; 280 | } 281 | 282 | track.setThumbnailFromURL(args.url) 283 | }) 284 | 285 | command.createCommand('delthumbnail') 286 | .help('Deletes thumbnail of currently playing track') 287 | .manual('Deletes thumbnail of currently playing track') 288 | .checkPermission(hasEditFilePermission) 289 | .exec((client, args, reply) => { 290 | let track = media.getCurrentTrack() 291 | 292 | if (!track) { 293 | reply('no track playing') 294 | return; 295 | } 296 | 297 | track.removeThumbnail() 298 | }) 299 | }) 300 | 301 | /** 302 | * Checks if a client has the necessary permissons 303 | * @param {Client} client 304 | * @returns {boolean} true if client has permission 305 | * @requires engine 306 | */ 307 | function hasEditFilePermission(client) { 308 | // try to find a sinusbot user that matches 309 | let matches = engine.getUsers().filter(user => 310 | // does the UID match? 311 | user.tsUid() == client.uid() || 312 | // or does a group ID match? 313 | client.getServerGroups().map(group => group.id()).includes(user.tsGroupId()) 314 | ) 315 | 316 | return matches.some(user => { 317 | // edit file permissions? 318 | return (user.privileges() & (1 << 4)) != 0 319 | }) 320 | } 321 | }) -------------------------------------------------------------------------------- /custom_commands.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Forum: https://forum.sinusbot.com/resources/simple-custom-commands.226/ 3 | * GitHub: https://github.com/irgendwr/sinusbot-scripts 4 | * 5 | * @author Jonas Bögle 6 | * @license MIT 7 | * 8 | * MIT License 9 | * 10 | * Copyright (c) 2019 Jonas Bögle 11 | * 12 | * Permission is hereby granted, free of charge, to any person obtaining a copy 13 | * of this software and associated documentation files (the "Software"), to deal 14 | * in the Software without restriction, including without limitation the rights 15 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | * copies of the Software, and to permit persons to whom the Software is 17 | * furnished to do so, subject to the following conditions: 18 | * 19 | * The above copyright notice and this permission notice shall be included in all 20 | * copies or substantial portions of the Software. 21 | * 22 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 28 | * SOFTWARE. 29 | * @ignore 30 | */ 31 | registerPlugin({ 32 | name: 'Custom commands', 33 | version: '1.4.0', 34 | description: 'Create your own custom commands.', 35 | author: 'Jonas Bögle (@irgendwr)', 36 | backends: ['ts3', 'discord'], 37 | vars: [ 38 | { 39 | name: 'commands', 40 | title: 'Commands', 41 | type: 'array', 42 | vars: [ 43 | { 44 | name: 'name', 45 | title: 'Command Name', 46 | placeholder: 'example: "info"', 47 | type: 'string', 48 | }, 49 | { 50 | name: 'description', 51 | title: 'Description', 52 | placeholder: 'example: "Responds with the users uid."', 53 | type: 'string', 54 | }, 55 | { 56 | name: 'prefix_change', 57 | title: 'Change Command Prefix (default is in Instance Settings)', 58 | type: 'checkbox', 59 | default: false, 60 | }, 61 | { 62 | name: 'prefix', 63 | title: 'Command Prefix', 64 | placeholder: 'example: "!"', 65 | type: 'string', 66 | indent: 2, 67 | conditions: [{ field: 'prefix_change', value: true }], 68 | }, 69 | { 70 | name: 'actions', 71 | title: 'Actions', 72 | type: 'array', 73 | vars: [ 74 | { 75 | name: 'action_type', 76 | title: 'What should happen?', 77 | type: 'select', 78 | options: [ 79 | 'Respond with a message', 80 | 'Play a Track', 81 | ], 82 | default: '0', 83 | }, 84 | { 85 | name: 'response_type', 86 | title: 'Where should the bot respond?', 87 | type: 'select', 88 | options: [ 89 | 'In the same chat as the user wrote in', 90 | 'Always answer in private chat' 91 | ], 92 | default: '0', 93 | conditions: [{ field: 'action_type', value: '0' }], 94 | }, 95 | { 96 | name: 'response_text', 97 | title: 'Response', 98 | type: 'multiline', 99 | placeholder: 'example: "Hi {mention}, your uid is: {uid}', 100 | conditions: [{ field: 'action_type', value: '0' }], 101 | }, 102 | { 103 | title: 'Placeholders (some are TS3 only):\n username, mention, uid, dbid, description, ping, total_connections, packetloss, bytes_sent, bytes_received, ip, first_join, os, version, clients_count, clients, channels_count, channels, client_groups_count, client_groups, server_groups_count, server_groups, playing, random(, ), randomString(option one, option two, ...), channel_name, channel_id, channel_url, client_channel_name, client_channel_id, client_channel_url', 104 | conditions: [{ field: 'action_type', value: '0' }], 105 | }, 106 | { 107 | name: 'track', 108 | title: 'Track', 109 | type: 'track', 110 | conditions: [{ field: 'action_type', value: '1' }], 111 | }, 112 | ] 113 | }, 114 | { 115 | name: 'restrict', 116 | title: 'Restrict usage?', 117 | type: 'checkbox', 118 | default: false, 119 | }, 120 | /*{ 121 | name: 'disable_serverchat', 122 | title: 'Disable in serverchat', 123 | type: 'checkbox', 124 | indent: 2, 125 | default: false, 126 | conditions: [{ field: 'restrict', value: true }], 127 | }, 128 | { 129 | name: 'disable_channelchat', 130 | title: 'Disable in channel chat', 131 | type: 'checkbox', 132 | indent: 2, 133 | default: false, 134 | conditions: [{ field: 'restrict', value: true }], 135 | }, 136 | { 137 | name: 'disable_privatechat', 138 | title: 'Disable in private chat', 139 | type: 'checkbox', 140 | indent: 2, 141 | default: false, 142 | conditions: [{ field: 'restrict', value: true }], 143 | },*/ 144 | { 145 | name: 'servergroups_type', 146 | title: 'Servergroups: Blacklist or whitelist?', 147 | type: 'select', 148 | options: [ 149 | 'Blacklist', 150 | 'Whitelist' 151 | ], 152 | default: '0', 153 | indent: 2, 154 | conditions: [{ field: 'restrict', value: true }], 155 | }, 156 | { 157 | name: 'servergroups_list', 158 | title: 'Servergroup IDs (Black-/Whitelist)', 159 | type: 'strings', 160 | indent: 2, 161 | conditions: [{ field: 'restrict', value: true }], 162 | }, 163 | { 164 | name: 'cooldown', 165 | title: 'Cooldown: Only allow command every X seconds (enter 0 to disable)', 166 | placeholder: '0', 167 | type: 'number', 168 | default: 0, 169 | indent: 2, 170 | conditions: [{ field: 'restrict', value: true }], 171 | }, 172 | ] 173 | } 174 | ] 175 | }, function (_, config, meta) { 176 | const event = require('event'); 177 | const audio = require('audio'); 178 | const media = require('media'); 179 | const engine = require('engine'); 180 | const backend = require('backend'); 181 | 182 | engine.log(`Loaded ${meta.name} v${meta.version} by ${meta.author}.`); 183 | 184 | /** 185 | * @typedef Context 186 | * @type {object} 187 | * @property {Client} client 188 | * @property {object} args 189 | * @property {(msg: string) => void} reply 190 | * @property {Message} ev 191 | * @property {DiscordMessage} message 192 | */ 193 | 194 | const placeholders = { 195 | username: ctx => ctx.client.name(), 196 | uid: ctx => ctx.client.uid(), 197 | dbid: ctx => ctx.client.databaseID(), 198 | description: ctx => ctx.client.description(), 199 | ping: ctx => ctx.client.getPing(), 200 | total_connections: ctx => ctx.client.getTotalConnections(), 201 | packetloss: ctx => ctx.client.getPacketLoss(), 202 | bytes_sent: ctx => ctx.client.getBytesSent(), 203 | bytes_received: ctx => ctx.client.getBytesReceived(), 204 | ip: ctx => ctx.client.getIPAddress(), 205 | first_join: ctx => ctx.client.getCreationTime() > 0 ? new Date(ctx.client.getCreationTime()).toLocaleDateString() : 'unknown', 206 | os: ctx => ctx.client.getPlatform(), 207 | version: ctx => ctx.client.getVersion(), 208 | mention: ctx => ctx.client.getURL(), 209 | clients_count: ctx => backend.getClients().length, 210 | clients: ctx => backend.getClients().map(client => client.name()).join(', '), 211 | channels_count: ctx => backend.getChannels().length, 212 | channels: ctx => backend.getChannels().map(channel => channel.name()).join(', '), 213 | client_groups_count: ctx => ctx.client.getServerGroups().length, 214 | client_groups: ctx => ctx.client.getServerGroups().map(group => group.name()).join(', '), 215 | server_groups_count: ctx => backend.getServerGroups().length, 216 | server_groups: ctx => backend.getServerGroups().map(group => group.name()).join(', '), 217 | playing: ctx => { 218 | let track = media.getCurrentTrack(); 219 | return track && audio.isPlaying() ? formatTrack(track) : 'none'; 220 | }, 221 | random: (ctx, ...args) => { 222 | let min = parseInt(args[0], 10); 223 | let max = parseInt(args[1], 10); 224 | return getRandomIntInclusive(min, max); 225 | }, 226 | randomString: (ctx, ...args) => { 227 | if (args.length === 0) return ""; 228 | return args[Math.floor(Math.random() * args.length)]; 229 | }, 230 | channel_name: ctx => { 231 | let channel = ctx.channel; 232 | return channel ? channel.name() : 'none'; 233 | }, 234 | channel_id: ctx => { 235 | let channel = ctx.channel; 236 | return channel ? channel.id() : 'none'; 237 | }, 238 | channel_url: ctx => { 239 | let channel = ctx.channel; 240 | if (!channel) return 'none'; 241 | return engine.getBackend() === 'discord' ? `https://www.discordapp.com/channels/${channel.id()}` : channel.getURL(); 242 | }, 243 | client_channel_name: ctx => { 244 | let channels = ctx.client.getChannels(); 245 | return channels && channels.lenght !== 0 ? channels[0].name() : 'none'; 246 | }, 247 | client_channel_id: ctx => { 248 | let channels = ctx.client.getChannels(); 249 | return channels && channels.lenght !== 0 ? channels[0].id() : 'none'; 250 | }, 251 | client_channel_url: ctx => { 252 | let channels = ctx.client.getChannels(); 253 | if (!channels || channels.lenght == 0) return 'none'; 254 | return engine.getBackend() === 'discord' ? `https://www.discordapp.com/channels/${channels[0].id()}` : channels[0].getURL(); 255 | }, 256 | }; 257 | 258 | event.on('load', () => { 259 | const command = require('command'); 260 | if (!command) { 261 | engine.log('command.js library not found! Please download command.js to your scripts folder and restart the SinusBot, otherwise this script will not work.'); 262 | engine.log('command.js can be found here: https://github.com/Multivit4min/Sinusbot-Command/blob/master/command.js'); 263 | return; 264 | } 265 | 266 | if (!config.commands || config.commands.length == 0) { 267 | engine.log("No commands configured."); 268 | return; 269 | } 270 | 271 | config.commands.forEach((cfg, i) => { 272 | let name = cfg.name; 273 | 274 | if (!name || name.length < 1) { 275 | engine.log(`Skipping ${i+1}. command: Name is not set.`); 276 | } 277 | 278 | let cmd; 279 | try { 280 | cmd = command.createCommand(name); 281 | engine.log(`Added command: ${name}`); 282 | } catch(ex) { 283 | engine.log(`Skipping ${i+1}. command due to error: ${ex}`); 284 | return; 285 | } 286 | 287 | let description = cfg.description; 288 | if (description) { 289 | cmd.help(description); 290 | cmd.manual(description); 291 | } 292 | 293 | if (cfg.prefix_change) { 294 | let prefix = cfg.prefix; 295 | if (!prefix || prefix.length < 1) { 296 | engine.log(`Warning in ${i+1}. command: Prefix not set, using default instead.`); 297 | } else { 298 | cmd.forcePrefix(prefix); 299 | } 300 | } 301 | 302 | if (cfg.restrict) { 303 | cmd.checkPermission(client => { 304 | const BLACKLIST = 0; 305 | let sglist = cfg.servergroups_list; 306 | let isBlacklist = (cfg.servergroups_type == BLACKLIST); 307 | 308 | if (!sglist || sglist.length == 0) { 309 | // returns true if (blacklist and empty) or false if (whitelist and empty) 310 | return isBlacklist 311 | } 312 | 313 | // returns true if (blacklist and not included) or (whitelist and included) 314 | return isBlacklist != client.getServerGroups().some(servergroup => 315 | sglist.some(sgid => 316 | servergroup.id() == sgid 317 | ) 318 | ) 319 | }) 320 | 321 | if (cfg.cooldown) { 322 | const throttle = command.createThrottle() 323 | .initialPoints(1) 324 | .restorePerTick(1) 325 | .tickRate(cfg.cooldown * 1000); //ms 326 | 327 | cmd.addThrottle(throttle); 328 | } 329 | } 330 | 331 | let actions = cfg.actions; 332 | if (!actions || actions.length < 1) { 333 | engine.log(`Warning in ${i+1}. command: No actions set.`); 334 | return; 335 | } else { 336 | let funcs = []; 337 | actions.forEach((action, j) => { 338 | switch (action.action_type) { 339 | case 0: 340 | case '0': // Respond with a message 341 | let text = action.response_text; 342 | if (!text || text.length < 1) { 343 | engine.log(`Warning in ${i+1}. command: No message text set in ${j+1}. action.`); 344 | return; 345 | } 346 | 347 | let type = action.response_type || '0'; 348 | if (type == '1') { // private chat 349 | funcs.push(ctx => { 350 | ctx.client.chat(format(text, ctx, placeholders)); 351 | }); 352 | } else { // same chat 353 | funcs.push(ctx => { 354 | ctx.reply(format(text, ctx, placeholders)); 355 | }); 356 | } 357 | break; 358 | case 1: 359 | case '1': // Play a Track 360 | let track = action.track; 361 | if (!track) { 362 | engine.log(`Warning in ${i+1}. command: No track set in ${j+1}. action.`); 363 | return; 364 | } 365 | funcs.push(() => { 366 | media.playURL(track); 367 | }); 368 | break; 369 | default: 370 | engine.log(`Warning in ${i+1}. command: ${j+1}. action is unknown.`); 371 | } 372 | }); 373 | cmd.exec((client, args, reply, ev) => { 374 | /*if (cfg.restrict) { 375 | if (ev.mode == 3 && cfg.disable_serverchat) { 376 | engine.log('Ignoring command since serverchat is disabled.'); 377 | return; 378 | } 379 | if (ev.mode == 2 && cfg.disable_channelchat) { 380 | engine.log('Ignoring command since channelchat is disabled.'); 381 | return; 382 | } 383 | if (ev.mode == 1 && cfg.disable_privatechat) { 384 | engine.log('Ignoring command since privatechat is disabled.'); 385 | return; 386 | } 387 | }*/ 388 | 389 | /** @implements {Context} */ 390 | let ctx = { 391 | client: client, 392 | args: args, 393 | reply: reply, 394 | ev: ev, 395 | channel: ev.channel, 396 | message: ev.message, 397 | } 398 | 399 | funcs.forEach(func => { 400 | try { 401 | func(ctx); 402 | } catch(ex) { 403 | engine.log(ex); 404 | } 405 | }); 406 | }) 407 | } 408 | }); 409 | }); 410 | 411 | /** 412 | * Formats a string with placeholders. 413 | * @param {string} str Format String 414 | * @param {Context} ctx Context 415 | * @param {object} placeholders Placeholders 416 | */ 417 | function format(str, ctx, placeholders) { 418 | return str.replace(/((?:[^{}]|(?:\{\{)|(?:\}\}))+)|(?:\{([0-9_\-a-zA-Z]+)(?:\((.+)\))?\})/g, (m, str, placeholder, argsStr) => { 419 | if (str) { 420 | return str.replace(/(?:{{)|(?:}})/g, m => m[0]); 421 | } else { 422 | if (!placeholders.hasOwnProperty(placeholder) || typeof placeholders[placeholder] !== 'function') { 423 | engine.log(`Unknown placeholder: ${placeholder}`); 424 | return `{${placeholder}: unknown placeholder}`; 425 | } 426 | let args = []; 427 | if (argsStr && argsStr.length > 0) { 428 | args = argsStr.split(/\s*(? arg.replace('\\,', ',')); 430 | } 431 | 432 | let result = `{${placeholder}: empty}`; 433 | try { 434 | result = placeholders[placeholder](ctx, ...args); 435 | } catch(ex) { 436 | result = `{${placeholder}: error}`; 437 | engine.log(`placeholder "${placeholder}" caused an error: ${ex}`); 438 | } 439 | 440 | return result; 441 | } 442 | }); 443 | } 444 | 445 | /** 446 | * @returns a random integer between two values, inclusive 447 | * @param {number} min 448 | * @param {number} max 449 | */ 450 | function getRandomIntInclusive(min, max) { 451 | min = Math.ceil(min); 452 | max = Math.floor(max); 453 | return Math.floor(Math.random() * (max - min + 1)) + min; 454 | } 455 | 456 | /** 457 | * Returns a formatted string from a track. 458 | * 459 | * @param {Track} track 460 | * @returns {string} formatted string 461 | */ 462 | function formatTrack(track) { 463 | let title = track.tempTitle() || track.title(); 464 | let artist = track.tempArtist() || track.artist(); 465 | return artist ? `${artist} - ${title}` : title; 466 | } 467 | }) -------------------------------------------------------------------------------- /discord_moderation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Forum: 3 | * GitHub: https://github.com/irgendwr/sinusbot-scripts 4 | */ 5 | 6 | registerPlugin({ 7 | name: 'Discord Moderation', 8 | version: '1.0.0', 9 | description: 'Adds commands for moderators on discord.', 10 | author: 'Jonas Bögle (irgendwr)', 11 | engine: '>= 1.0.0', 12 | backends: ['discord'], 13 | requiredModules: ['discord-dangerous'], 14 | vars: [{ 15 | name: "admins", 16 | title: "UIDs of users which have access to admin commands", 17 | type: "strings", 18 | default: [] 19 | }] 20 | }, (_, config, meta) => { 21 | const event = require('event') 22 | const engine = require('engine') 23 | const backend = require('backend') 24 | 25 | engine.log(`Loaded ${meta.name} v${meta.version} by ${meta.author}.`) 26 | 27 | event.on('load', () => { 28 | const command = require("command") 29 | if (!command) { 30 | engine.log('command.js library not found! Please download command.js to your scripts folder and restart the SinusBot, otherwise this script will not work.'); 31 | engine.log('command.js can be found here: https://github.com/Multivit4min/Sinusbot-Command/blob/master/command.js'); 32 | return; 33 | } 34 | const {createCommand, createArgument} = command 35 | 36 | createCommand('clear') 37 | .help('Clears messages') 38 | .manual('Clears messages') 39 | .checkPermission(allowAdminCommands) 40 | // @ts-ignore 41 | .addArgument(createArgument("number").setName("amount").min(1).max(100).optional(10)) 42 | .exec((client, { amount }, reply, /** @implements {Message} */ev) => { 43 | let msg = ev.message 44 | if (!msg) return engine.log('no message object found, update command.js or something') 45 | 46 | backend.extended().rawCommand('GET', `/channels/${msg.channelID()}/messages?before=${msg.ID()}&limit=${amount}`, null, (err, data) => { 47 | if (err) { 48 | reply('Error: ' + err) 49 | engine.log(err) 50 | return; 51 | } 52 | 53 | let res = JSON.parse(data); 54 | if (!res || !res.length) { 55 | reply('Error: Invalid API repsonse') 56 | engine.log('Invalid response: '+data) 57 | } 58 | 59 | 60 | let old = []; 61 | const twoWeeks = 2 * 7 * 24 * 60 * 60 * 1000//ms 62 | res = res.filter(msg => { 63 | if (Date.now()-Date.parse(msg.timestamp) >= twoWeeks) { 64 | old.push(msg) 65 | return false 66 | } 67 | return true 68 | }) 69 | 70 | let ids = res.map(msg => msg.id) 71 | if (ids.length < 100 && ids.length !== 0) { 72 | ids.push(msg.ID()) 73 | } else { 74 | msg.delete() 75 | } 76 | 77 | backend.extended().rawCommand('POST', `/channels/${msg.channelID()}/messages/bulk-delete`, {messages: ids}, err => { 78 | if (err) { 79 | reply('Error: ' + err) 80 | engine.log(err) 81 | } 82 | }) 83 | 84 | if (old.length !== 0) { 85 | msg.reply('Messages older than two weeks cannot be deleted in bulk.') 86 | } 87 | }) 88 | }) 89 | }) 90 | 91 | /** 92 | * Checks if a client is allowed to use admin commands. 93 | * @param {Client} client 94 | * @returns {boolean} 95 | */ 96 | function allowAdminCommands(client) { 97 | return config.admins.includes(client.uid().split("/")[1]) 98 | } 99 | 100 | /** 101 | * Get a guild Member 102 | * @param {Client} client 103 | * @returns {Promise} 104 | */ 105 | function getGuildMember(client) { 106 | let [guildId, userId] = client.id().split('/'); 107 | 108 | return discord('POST', `/guilds/${guildId}/members/${userId}`) 109 | } 110 | 111 | /** 112 | * Executes a discord API call 113 | * @param {string} method http method 114 | * @param {string} path path 115 | * @param {object} [data] json data 116 | * @param {boolean} [repsonse] `true` if you're expecting a json response, `false` otherwise 117 | * @return {Promise} 118 | */ 119 | function discord(method, path, data, repsonse=true) { 120 | return new Promise((resolve, reject) => { 121 | backend.extended().rawCommand(method, path, data, (err, data) => { 122 | if (err) return reject(err) 123 | if (repsonse) { 124 | let res 125 | try { 126 | res = JSON.parse(data) 127 | } catch (err) { 128 | return reject(err) 129 | } 130 | 131 | if (res === undefined) { 132 | return reject('Invalid Response') 133 | } 134 | 135 | return resolve(res) 136 | } 137 | resolve() 138 | }) 139 | }) 140 | } 141 | }) -------------------------------------------------------------------------------- /discord_rename_everyone.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Forum: 3 | * GitHub: https://github.com/irgendwr/sinusbot-scripts 4 | */ 5 | 6 | registerPlugin({ 7 | name: 'Rename EVERYONE', 8 | version: '1.0.0', 9 | description: 'Allows you to rename everyone in your discord server.', 10 | author: 'Jonas Bögle (irgendwr)', 11 | engine: '>= 1.0.0', 12 | backends: ['discord'], 13 | //requiredModules: ['discord-dangerous'], 14 | vars: [ 15 | { 16 | name: 'guild', 17 | title: 'Guild ID', 18 | type: 'string', 19 | }, 20 | { 21 | name: 'name', 22 | title: 'Name', 23 | type: 'string', 24 | }, 25 | { 26 | name: "admins", 27 | title: "UIDs of users which have access to admin commands", 28 | type: "strings", 29 | default: [] 30 | }, 31 | { 32 | name: 'delay', 33 | title: 'Delay (should normally be 30 seconds due to rate limiting)', 34 | type: 'number', 35 | default: 30, 36 | placeholder: 30, 37 | }, 38 | { 39 | name: 'invert', 40 | title: 'Invert List', 41 | type: 'checkbox', 42 | default: false, 43 | }, 44 | { 45 | name: 'join', 46 | title: 'Rename on join', 47 | type: 'checkbox', 48 | default: false, 49 | }, 50 | ] 51 | }, (_, config, meta) => { 52 | const event = require('event') 53 | const engine = require('engine') 54 | const backend = require('backend') 55 | const store = require('store') 56 | 57 | if (!config.guild) { 58 | engine.log('Please set the guild ID in the config.'); 59 | return; 60 | } 61 | /*if (!config.name) { 62 | engine.log('Please set the name in the config.'); 63 | return; 64 | }*/ 65 | 66 | // see https://discordapp.com/developers/docs/topics/gateway#rate-limiting 67 | const DELAY = (config.delay || 30) * 1000; 68 | 69 | let stop = false; 70 | let previousName; 71 | let previousNicks = store.getInstance('previousNicks') || []; 72 | 73 | event.on('load', () => { 74 | const command = require('command') 75 | if (!command) { 76 | engine.log('command.js library not found! Please download command.js to your scripts folder and restart the SinusBot, otherwise this script will not work.'); 77 | engine.log('command.js can be found here: https://github.com/Multivit4min/Sinusbot-Command/blob/master/command.js'); 78 | return; 79 | } 80 | 81 | command.createCommand('rename-everyone') 82 | .alias('RE') 83 | .addArgument(command.createArgument('rest').setName('name').optional()) 84 | .help('Rename everyone') 85 | .manual('Renames everyone.') 86 | .checkPermission(allowAdminCommands) 87 | .exec((client, args, reply, ev) => { 88 | if (args.name && args.name !== '') { 89 | previousName = config.name; 90 | config.name = args.name; 91 | //engine.saveConfig(config); 92 | } 93 | 94 | engine.log(`renaming everyone to ${config.name}...`) 95 | reply(`${config.name}? Sounds good!\nPlease wait, this may take very *very* long...`) 96 | 97 | stop = false; 98 | renameEveryone(config.name).then(() => { 99 | engine.log(`done`) 100 | engine.log(`${previousNicks.length} entries stored`); 101 | reply(`Done! ${previousNicks.length} entries stored, see log for more.`); 102 | }) 103 | }) 104 | 105 | command.createCommand('rename-everyone-clear') 106 | .alias('RE-clear') 107 | .help('Rename everyone') 108 | .manual('Renames everyone.') 109 | .checkPermission(allowAdminCommands) 110 | .exec((client, args, reply, ev) => { 111 | stop = true; 112 | previousName = config.name; 113 | config.name = ""; 114 | config.join = false; 115 | //engine.saveConfig(config); 116 | 117 | engine.log(`clearing everyones nickname...`) 118 | reply(`Clearing everyones nickname.\nPlease wait, this may take very *very* long...`) 119 | 120 | stop = false; 121 | renameEveryone(config.name).then(() => { 122 | engine.log(`done`) 123 | reply(`Done!`); 124 | }) 125 | }) 126 | 127 | command.createCommand('rename-everyone-stop') 128 | .alias('RE-stop') 129 | .help('Stop renaming everyone') 130 | .manual('Stops renaming everyone.') 131 | .checkPermission(allowAdminCommands) 132 | .exec((client, args, reply, ev) => { 133 | if (stop) { 134 | return reply('Stopped already.') 135 | } 136 | 137 | stop = true; 138 | reply('okay') 139 | }) 140 | 141 | command.createCommand('rename-everyone-previous') 142 | .alias('RE-previous') 143 | .help('Log previous nichnames') 144 | .manual('Logs previous nichnames.') 145 | .checkPermission(allowAdminCommands) 146 | .exec((client, args, reply, ev) => { 147 | engine.log(`${previousNicks.length} entries found`); 148 | engine.log(JSON.stringify(previousNicks)); 149 | reply(`${previousNicks.length} entries found, see log for more`); 150 | }) 151 | 152 | command.createCommand('rename-everyone-previous-rm') 153 | .alias('RE-previous-rm') 154 | .help('Remove previous nichnames') 155 | .manual('Removes previous nichnames.') 156 | .checkPermission(allowAdminCommands) 157 | .exec((client, args, reply, ev) => { 158 | previousNicks = []; 159 | store.unsetInstance('previousNicks'); 160 | reply('done'); 161 | }) 162 | }) 163 | 164 | if (config.join) { 165 | // NOTE: I don't know why, but these don't work :( 166 | 167 | event.on('discord:GUILD_MEMBERADD', member => { 168 | engine.log(`processing member: ${member.user.username} ${member.nick ? `(${member.nick})` : ''}`); 169 | processMember(member, config.nick); 170 | }); 171 | 172 | event.on('clientMove', ev => { 173 | let id = ev.client.uid().split('/'); 174 | if (id[0] != config.guild) return; 175 | 176 | getMember(config.guild, id[1]).then(member => { 177 | engine.log(`processing member: ${member.user.username} ${member.nick ? `(${member.nick})` : ''}`); 178 | processMember(member, config.nick) 179 | }); 180 | }); 181 | event.on('clientVisible', ev => { 182 | let id = ev.client.uid().split('/'); 183 | if (id[0] != config.guild) return; 184 | 185 | getMember(config.guild, id[1]).then(member => { 186 | engine.log(`processing member: ${member.user.username} ${member.nick ? `(${member.nick})` : ''}`); 187 | processMember(member, config.nick) 188 | }); 189 | }); 190 | event.on('chat', ev => { 191 | let id = ev.client.uid().split('/'); 192 | if (id[0] != config.guild) return; 193 | 194 | getMember(config.guild, id[1]).then(member => { 195 | if (processMember(member, config.nick)) { 196 | engine.log(`renamed member: ${member.user.username} ${member.nick ? `(${member.nick})` : ''}`); 197 | } 198 | }); 199 | }); 200 | } 201 | 202 | /** 203 | * Rename everyone. 204 | * @param {string} nick Nickname 205 | */ 206 | async function renameEveryone(nick) { 207 | let last = 0; 208 | while (!stop && last >= 0) { 209 | last = await getMembers(config.guild, 1000, last).then(members => processMemberList(members, nick)) 210 | engine.log(`last: ${last}`) 211 | } 212 | 213 | store.setInstance('previousNicks', previousNicks); 214 | } 215 | 216 | /** 217 | * Process a member list. 218 | * @param {object[]} members see https://discordapp.com/developers/docs/resources/guild#guild-member-object 219 | * @param {string} nick Nickname 220 | */ 221 | async function processMemberList(members, nick) { 222 | engine.log(`processing ${members.length} members...`) 223 | 224 | // sort ASC by join date 225 | members.sort(function(a, b) { 226 | var joined_atA = a.joined_at.toUpperCase(); 227 | var joined_atB = b.joined_at.toUpperCase(); 228 | if (joined_atA < joined_atB) { 229 | return -1; 230 | } 231 | if (joined_atA > joined_atB) { 232 | return 1; 233 | } 234 | 235 | // joined_at must be equal 236 | return 0; 237 | }); 238 | 239 | if (config.invert) { 240 | engine.log('inverting array') 241 | members.reverse(); 242 | } 243 | 244 | if (members.length === 0) { 245 | return -1; 246 | } 247 | 248 | let highestID = 0; 249 | for (let member of members) { 250 | if (stop) { 251 | engine.log('Stopping.') 252 | return -2; 253 | } 254 | 255 | //engine.log(`member: ${JSON.stringify(member)}`); 256 | //engine.log(`processing member: ${member.user.username} ${member.nick ? `(${member.nick})` : ''}`); 257 | let renamed = processMember(member, nick); 258 | 259 | if (member.user.id > highestID) highestID = member.user.id; 260 | 261 | if (renamed) { 262 | engine.log(`renamed member: ${member.user.username} ${member.nick ? `(${member.nick})` : ''}`); 263 | await wait(DELAY); 264 | } 265 | } 266 | 267 | return highestID; 268 | } 269 | 270 | /** 271 | * Process a member. Set nick, if it doesn't match. 272 | * @param {object} member see https://discordapp.com/developers/docs/resources/guild#guild-member-object 273 | * @param {string} nick Nickname 274 | * @returns {boolean} if member was renamed 275 | */ 276 | function processMember(member, nick) { 277 | if ((member.nick && member.nick == nick) || (!nick && !member.nick)) { 278 | return false; 279 | } 280 | 281 | if (member.nick && member.nick !== previousName) { 282 | previousNicks.push({ 283 | id: member.user.id, 284 | nick: member.nick, 285 | username: member.user.username, 286 | discriminator: member.user.discriminator, 287 | }); 288 | } 289 | 290 | setNick(config.guild, member.user.id, nick).catch(err => engine.log(err)); 291 | 292 | return true; 293 | } 294 | 295 | /** 296 | * Wait until resolving promise. 297 | * @param {number} delay Delay in ms 298 | * @param {any} [value] Value passed to resolve 299 | */ 300 | function wait(delay, value) { 301 | return new Promise(resolve => { 302 | setTimeout(() => { 303 | resolve(value); 304 | }, delay); 305 | }); 306 | } 307 | 308 | /** 309 | * Gets a user object. 310 | * @param {(string|number)} guildID Guild ID 311 | * @param {(string|number)} userID User ID 312 | * @param {string} nick Nickname 313 | * @return {Promise} 314 | */ 315 | function setNick(guildID, userID, nick) { 316 | return discord('PATCH', `/guilds/${guildID}/members/${userID}`, {nick: nick}, false) 317 | } 318 | 319 | /** 320 | * Returns a list of guild member objects that are members of the guild. 321 | * @param {(string|number)} guildID Guild ID 322 | * @param {number} [limit] Max number of members to return (1-1000) 323 | * @param {(string|number)} [after] Highest user id in the previous page 324 | * @return {Promise} 325 | * @author Jonas Bögle 326 | * @license MIT 327 | */ 328 | function getMembers(guildID, limit=1, after=0) { 329 | return discord('GET', `/guilds/${guildID}/members?limit=${limit}&after=${after}`, null, true) 330 | } 331 | 332 | /** 333 | * Returns a list of guild member objects that are members of the guild. 334 | * @param {(string|number)} guildID Guild ID 335 | * @param {(string|number)} userID User ID 336 | * @return {Promise} 337 | * @author Jonas Bögle 338 | * @license MIT 339 | */ 340 | function getMember(guildID, userID) { 341 | return discord('GET', `/guilds/${guildID}/members/${userID}`, null, true) 342 | } 343 | 344 | /** 345 | * Executes a discord API call 346 | * @param {string} method http method 347 | * @param {string} path path 348 | * @param {object} [data] json data 349 | * @param {boolean} [repsonse] `true` if you're expecting a json response, `false` otherwise 350 | * @return {Promise} 351 | * @author Jonas Bögle 352 | * @license MIT 353 | */ 354 | function discord(method, path, data, repsonse=true) { 355 | //engine.log(`${method} ${path}`) 356 | 357 | return new Promise((resolve, reject) => { 358 | backend.extended().rawCommand(method, path, data, (err, data) => { 359 | if (err) return reject(err) 360 | if (repsonse) { 361 | let res 362 | try { 363 | res = JSON.parse(data) 364 | } catch (err) { 365 | engine.log(`${method} ${path} failed`) 366 | engine.log(`${data}`) 367 | return reject(err) 368 | } 369 | 370 | if (res === undefined) { 371 | engine.log(`${method} ${path} failed`) 372 | engine.log(`${data}`) 373 | return reject('Invalid Response') 374 | } 375 | 376 | return resolve(res) 377 | } 378 | resolve() 379 | }) 380 | }) 381 | } 382 | 383 | /** 384 | * Checks if a client is allowed to use admin commands. 385 | * @param {Client} client 386 | * @returns {boolean} 387 | */ 388 | function allowAdminCommands(client) { 389 | switch (engine.getBackend()) { 390 | case "discord": 391 | return config.admins.includes(client.uid().split("/")[1]) 392 | case "ts3": 393 | return config.admins.includes(client.uid()) 394 | default: 395 | throw new Error(`Unknown backend ${engine.getBackend()}`) 396 | } 397 | } 398 | }) -------------------------------------------------------------------------------- /discord_sinusbot.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Forum: 3 | * GitHub: https://github.com/irgendwr/sinusbot-scripts 4 | */ 5 | 6 | registerPlugin({ 7 | name: 'Discord SinusBot', 8 | version: '1.0.0', 9 | description: 'Useful commands for the official SinusBot Discord server.', 10 | author: 'Jonas Bögle (irgendwr)', 11 | engine: '>= 1.0.0', 12 | backends: ['discord'], 13 | requiredModules: ['http', 'discord-dangerous'], 14 | vars: [ 15 | { 16 | name: 'api_forum', 17 | title: 'Forum API URL', 18 | type: 'string', 19 | placeholder: 'https://forum.example.com/foobar?id=%ID%' 20 | }, 21 | ] 22 | }, (_, config, meta) => { 23 | const event = require('event') 24 | const engine = require('engine') 25 | const backend = require('backend') 26 | const http = require('http') 27 | 28 | engine.log(`Loaded ${meta.name} v${meta.version} by ${meta.author}.`) 29 | 30 | const urlPattern = /^https:\/\/forum\.sinusbot\.com\/members\/(?:.*\.)?(\d+)\/(?:#.*)?$/; 31 | const tagPattern = /^ ?<@!?\d{18}> ?$/; 32 | 33 | event.on('load', () => { 34 | const command = require('command') 35 | if (!command) { 36 | engine.log('command.js library not found! Please download command.js to your scripts folder and restart the SinusBot, otherwise this script will not work.'); 37 | engine.log('command.js can be found here: https://github.com/Multivit4min/Sinusbot-Command/blob/master/command.js'); 38 | return; 39 | } 40 | 41 | command.createCommand('needinfo') 42 | .forcePrefix('!') 43 | .addArgument(command.createArgument('string').setName('user').optional()) 44 | .help('Lists required information that we need to be able to help') 45 | .manual('Lists required information that we need to be able to help.') 46 | .exec((client, args, /** @type {(message: string)=>void} */reply, ev) => { 47 | let pre = 'Please **send us all of the information listed below**, depending on your OS.\n' 48 | if (tagPattern.exec(args.user) != null) { 49 | pre = `Hi ${args.user}! ` + pre 50 | } 51 | reply(pre + ` 52 | > :penguin: **Linux** 53 | 1) Output of the diagnostic script: 54 | 2) Instance log / SinusBot log (set \`LogLevel = 10\` in your \`config.ini\` before) 55 | Share these via to reduce spam. 56 | 57 | > :snail: **Windows** 58 | 1) OS (operating system), e.g. *Windows 10 64bit* 59 | 2) SinusBot version () 60 | 3) TeamSpeak Client version 61 | 4) Instance log / SinusBot log (set \`LogLevel = 10\` in your \`config.ini\` before) 62 | **Share these via ** to reduce spam. 63 | 64 | *This automated message was triggered by ${client.getURL()}*`) 65 | // try to delete original message to reduce spam 66 | ev.message.delete() 67 | }) 68 | 69 | command.createCommand('needinfo-linux') 70 | .alias('needinfolinux', 'linuxinfo', 'infolinux') 71 | .forcePrefix('!') 72 | .addArgument(command.createArgument('string').setName('user').optional()) 73 | .help('Lists required information that we need to be able to help') 74 | .manual('Lists required information that we need to be able to help.') 75 | .exec((client, args, /** @type {(message: string)=>void} */reply, ev) => { 76 | let pre = 'Please **send us all of the information listed below**.' 77 | if (tagPattern.exec(args.user) != null) { 78 | pre = `Hi ${args.user}! ` + pre 79 | } 80 | reply(pre + ` *This assumes you're using Linux* 81 | 82 | 1) **Send us the output of the diagnostic script**: 83 | \`\`\`bash 84 | # before running the diag script you might need to install depencencies 85 | apt-get install bc binutils coreutils lsb-release util-linux net-tools curl 86 | # change to your path: 87 | cd /opt/sinusbot/ 88 | # execute with root privileges, sudo may be required: 89 | bash <(wget -O - https://raw.githubusercontent.com/patschi/sinusbot-tools/master/tools/diagSinusbot.sh) 90 | \`\`\` 91 | 2) And the **Instance log / SinusBot log** (set \`LogLevel = 10\` in your \`config.ini\` before) 92 | **Share these via ** to reduce spam. 93 | 94 | *This automated message was triggered by ${client.getURL()}*`) 95 | // try to delete original message to reduce spam 96 | ev.message.delete() 97 | }) 98 | 99 | command.createCommand('needinfo-windows') 100 | .alias('needinfo-win', 'needinfowindows', 'needinfowin', 'windowsinfo', 'wininfo', 'infowin') 101 | .forcePrefix('!') 102 | .addArgument(command.createArgument('string').setName('user').optional()) 103 | .help('Lists required information that we need to be able to help') 104 | .manual('Lists required information that we need to be able to help.') 105 | .exec((client, args, /** @type {(message: string)=>void} */reply, ev) => { 106 | let pre = 'Please **send us all of the information listed below**.' 107 | if (tagPattern.exec(args.user) != null) { 108 | pre = `Hi ${args.user}! ` + pre 109 | } 110 | reply(pre + ` *This assumes you're using Windows* 111 | 112 | 1) OS (operating system), e.g. *Windows 10 64bit* 113 | 2) SinusBot version () 114 | 3) TeamSpeak Client version 115 | 4) Instance log / SinusBot log (set \`LogLevel = 10\` in your \`config.ini\` before) 116 | **Share the logs via to reduce spam.** 117 | 118 | *This automated message was triggered by ${client.getURL()}*`) 119 | // try to delete original message to reduce spam 120 | ev.message.delete() 121 | }) 122 | 123 | command.createCommand('diagscript') 124 | .forcePrefix('!') 125 | .help('SinusBot Documentation: diagscript') 126 | .manual('SinusBot Documentation: diagscript') 127 | .exec((client, args, /** @type {(message: string)=>void} */reply) => { 128 | reply(`\`\`\`bash 129 | # before running the diag script you might need to install depencencies 130 | apt-get install bc binutils coreutils lsb-release util-linux net-tools curl 131 | # change to your path: 132 | cd /opt/sinusbot/ 133 | # execute with root privileges, sudo may be required: 134 | bash <(wget -O - https://raw.githubusercontent.com/patschi/sinusbot-tools/master/tools/diagSinusbot.sh) 135 | \`\`\``) 136 | }) 137 | 138 | command.createCommand('install') 139 | .forcePrefix('!') 140 | .help('SinusBot Documentation: install') 141 | .manual('SinusBot Documentation: install') 142 | .exec((client, args, /** @type {(message: string)=>void} */reply) => { 143 | reply(`Linux: 144 | Windows: 145 | Docker: `) 146 | }) 147 | 148 | command.createCommand('youtube-dl') 149 | .alias('ytdldoc', 'ytdldocs') 150 | .forcePrefix('!') 151 | .help('SinusBot Documentation: youtube-dl') 152 | .manual('SinusBot Documentation: youtube-dl') 153 | .exec((client, args, /** @type {(message: string)=>void} */reply) => { 154 | reply(``) 155 | }) 156 | 157 | command.createCommand('docs') 158 | .forcePrefix('!') 159 | .help('SinusBot Documentation') 160 | .manual('SinusBot Documentation') 161 | .exec((client, args, /** @type {(message: string)=>void} */reply) => { 162 | reply(`SinusBot Documentation: `) 163 | }) 164 | 165 | command.createCommand('scripts') 166 | .alias('scripting') 167 | .forcePrefix('!') 168 | .help('SinusBot Documentation: Scripts') 169 | .manual('SinusBot Documentation: Scripts') 170 | .exec((client, args, /** @type {(message: string)=>void} */reply) => { 171 | reply(`About writing scripts: 172 | Scripting Documentation: 173 | Scripts: `) 174 | }) 175 | 176 | command.createCommand('license') 177 | .alias('lic') 178 | .forcePrefix('!') 179 | .help('SinusBot Documentation: Licenses') 180 | .manual('SinusBot Documentation: Licenses') 181 | .exec((client, args, /** @type {(message: string)=>void} */reply) => { 182 | reply(`Information about Licenses: `) 183 | }) 184 | 185 | command.createCommand('3rdparty') 186 | .alias('noscriptsupport', 'scriptsupport') 187 | .forcePrefix('!') 188 | .help('Reminder: no 3rd party support') 189 | .manual('Reminder: We don\'t offer support for 3rd-party scripts.') 190 | .exec((client, args, /** @type {(message: string)=>void} */reply) => { 191 | reply(`We don't offer support for 3rd-party scripts. Ask for help in the discussions thread of the script (in the forum) instead.`) 192 | }) 193 | 194 | command.createCommand('installer-error') 195 | .alias('installererror') 196 | .forcePrefix('!') 197 | .help('Reminder: Installer Script support in forum') 198 | .manual('Reminder: Issues with the Installer Script should be posted in it\'s forum thread.') 199 | .exec((client, args, /** @type {(message: string)=>void} */reply) => { 200 | reply(`Issues with the Installer Script should be posted in it's forum thread: `) 201 | }) 202 | 203 | command.createCommand('roles') 204 | .alias('groups') 205 | .forcePrefix('!') 206 | .addArgument(command.createArgument('string').setName('url')) 207 | .help('Gives you the groups from the SinusBot Forum') 208 | .manual('Gives you the groups from the SinusBot Forum.\nThis only works if you set your full discord username (for example: `irgendwr#7476`) in your forum settings: .') 209 | .exec((/** @type {Client} */client, /** @type {object} */args, /** @type {(message: string)=>void} */reply) => { 210 | if (!args.url) { 211 | getUser(client).then(user => { 212 | const tag = user.username + '#' + user.discriminator 213 | reply(`To get the groups from your forum account you need to: 214 | 1) Set your full discord username \`${tag}\` in your forum settings: 215 | 2) Write \`!roles \` in <#534460311575461940>`) 216 | }) 217 | return; 218 | } 219 | 220 | let matches = urlPattern.exec(args.url) 221 | if (!matches || matches.length < 2) { 222 | reply('That\'s not a valid url.\nExample of a valid url: `https://forum.sinusbot.com/members/irgendwr.1213/`') 223 | return; 224 | } 225 | 226 | let id = matches[1]; 227 | http.simpleRequest({ 228 | 'method': 'GET', 229 | 'url': config.api_forum.replace(/%ID%/gi, encodeURIComponent(id)), 230 | 'timeout': 6000, 231 | }, function (error, response) { 232 | if (error) { 233 | engine.log("Error: " + error); 234 | reply('Error: API error #1 :sad:'); 235 | return; 236 | } 237 | 238 | if (response.statusCode != 200) { 239 | engine.log("HTTP Error: " + response.status); 240 | reply('Error: API error #2 :sad:'); 241 | return; 242 | } 243 | 244 | // parse JSON response 245 | var res; 246 | try { 247 | res = JSON.parse(response.data.toString()); 248 | } catch (err) { 249 | engine.log(err.message); 250 | } 251 | 252 | // check if parsing was successfull 253 | if (res === undefined) { 254 | engine.log("Invalid JSON."); 255 | reply('Error: invalid response :sad:'); 256 | return; 257 | } 258 | 259 | if (!res.success) { 260 | reply('Invalid user.') 261 | return; 262 | } 263 | 264 | getUser(client).then(user => { 265 | const tag = user.username + '#' + user.discriminator 266 | if (!res.discordID) { 267 | reply('No Discord ID found.\nPlease set your full discord username `'+tag+'` in your forum settings: ') 268 | return; 269 | } 270 | 271 | if (!tag || tag !== res.discordID) { 272 | reply('Your username `'+tag+'` does not match `'+res.discordID+'`.') 273 | return; 274 | } 275 | 276 | if (res.groups.length === 0) { 277 | reply('You don\'t have any groups in the forum :hatching_chick:') 278 | } 279 | 280 | engine.log('roles: ' + res.groups.join(', ')) 281 | 282 | let roles = []; 283 | if (res.groups.includes('Developer')) { 284 | roles.push(':floppy_disk: Developer') 285 | //addRole(client, '454967235157426178') 286 | } 287 | if (res.groups.includes('Moderating')) { 288 | roles.push(':hammer: Moderator') 289 | //addRole(client, '531495313291083802') 290 | } 291 | if (res.groups.includes('VIP')) { 292 | roles.push(':star: VIP') 293 | addRole(client, '454965825317896193') 294 | } 295 | if (res.groups.includes('Tier III')) { 296 | roles.push(':heart: Tier III') 297 | addRole(client, '681228347065499899') 298 | } 299 | if (res.groups.includes('Tier II')) { 300 | roles.push(':heart: Tier II') 301 | addRole(client, '681228203096145996') 302 | } 303 | if (res.groups.includes('Tier I')) { 304 | roles.push(':heart: Tier I') 305 | addRole(client, '624933507260612608') 306 | } 307 | if (res.groups.includes('Donor++')) { 308 | roles.push(':heart: Donor++') 309 | addRole(client, '454967925544321034') 310 | } 311 | if (res.groups.includes('Donor')) { 312 | roles.push(':heart: Donor') 313 | addRole(client, '452456955391377409') 314 | } 315 | if (res.groups.includes('Contributor')) { 316 | roles.push('Contributor') 317 | addRole(client, '472340215113973772') 318 | } 319 | if (res.groups.includes('Insider')) { 320 | roles.push('Insider') 321 | addRole(client, '452456498300452877') 322 | } 323 | 324 | const len = roles.length 325 | if (len !== 0) { 326 | reply(`Welcome ${client.getURL()}! :slight_smile: 327 | Added ${len} role${len == 1 ? '' : 's'} from account ${id}:\n${roles.join('\n')}`) 328 | engine.log(`${client.nick()} (${client.uid()}) synced roles from ${id}: ${roles.join()}`) 329 | } else { 330 | reply('You don\'t have any groups in the forum that can be snyced :confused:') 331 | } 332 | }) 333 | }); 334 | }) 335 | }) 336 | 337 | /** 338 | * Gets a user object. 339 | * @param {Client} client Client 340 | * @return {Promise} 341 | */ 342 | function getUser(client) { 343 | const id = client.uid().split('/')[1] 344 | return discord('GET', `/users/${id}`, null, true) 345 | } 346 | 347 | /** 348 | * Gets a user object. 349 | * @param {Client} client Client 350 | * @param {string} roleID Role ID 351 | * @return {Promise} 352 | */ 353 | function addRole(client, roleID) { 354 | const ids = client.uid().split('/') 355 | return discord('PUT', `/guilds/${ids[0]}/members/${ids[1]}/roles/${roleID}`, null, false) 356 | } 357 | 358 | /** 359 | * Executes a discord API call 360 | * @param {string} method http method 361 | * @param {string} path path 362 | * @param {object} [data] json data 363 | * @param {boolean} [repsonse] `true` if you're expecting a json response, `false` otherwise 364 | * @return {Promise} 365 | * @author Jonas Bögle 366 | * @license MIT 367 | */ 368 | function discord(method, path, data, repsonse=true) { 369 | //engine.log(`${method} ${path}`) 370 | 371 | return new Promise((resolve, reject) => { 372 | backend.extended().rawCommand(method, path, data, (err, data) => { 373 | if (err) return reject(err) 374 | if (repsonse) { 375 | let res 376 | try { 377 | res = JSON.parse(data) 378 | } catch (err) { 379 | return reject(err) 380 | } 381 | 382 | if (res === undefined) { 383 | return reject('Invalid Response') 384 | } 385 | 386 | return resolve(res) 387 | } 388 | resolve() 389 | }) 390 | }) 391 | } 392 | }) 393 | -------------------------------------------------------------------------------- /exec.js: -------------------------------------------------------------------------------- 1 | registerPlugin({ 2 | name: "Exec", 3 | version: "0.1.0", 4 | description: "Evaluates chat commands", 5 | author: "Multivitamin { 15 | const engine = require("engine") 16 | const event = require("event") 17 | const format = require("format") 18 | // import modules for quick use in exec: 19 | /* eslint-disable */ 20 | const backend = require("backend") 21 | const helpers = require("helpers") 22 | const media = require("media") 23 | const audio = require("audio") 24 | const store = require("store") 25 | const http = require("http") 26 | /* eslint-enable */ 27 | 28 | const codeBlockPattern = /^ *```(javascript *\r?\n?)?(?.*)``` *$/si 29 | 30 | /** 31 | * Checks if a client is allowed to use admin commands. 32 | * @param {Client} client 33 | * @returns {boolean} 34 | */ 35 | function allowAdminCommands(client) { 36 | switch (engine.getBackend()) { 37 | case "discord": 38 | return config.admins.includes(client.uid().split("/")[1]) 39 | case "ts3": 40 | return config.admins.includes(client.uid()) 41 | default: 42 | throw new Error(`Unknown backend ${engine.getBackend()}`) 43 | } 44 | } 45 | 46 | // eslint-disable-next-line no-unused-vars 47 | function evaluate(code, ev, reply) { 48 | const start = Date.now() 49 | let data = null 50 | let error = null 51 | try { 52 | const client = ev.client 53 | // eslint-disable-next-line no-eval 54 | data = eval(code) 55 | } catch (e) { 56 | error = e 57 | } 58 | return { 59 | error, 60 | data, 61 | duration: Date.now() - start 62 | } 63 | } 64 | 65 | event.on("load", () => { 66 | const command = require("command") 67 | if (!command) { 68 | engine.log('command.js library not found! Please download command.js to your scripts folder and restart the SinusBot, otherwise this script will not work.'); 69 | engine.log('command.js can be found here: https://github.com/Multivit4min/Sinusbot-Command/blob/master/command.js'); 70 | return; 71 | } 72 | const {createCommand} = command 73 | 74 | createCommand("exec") 75 | .alias('eval', 'run') 76 | .help("Executes a raw command within Sinusbot") 77 | .addArgument(arg => arg.rest.setName("code")) 78 | .checkPermission(allowAdminCommands) 79 | .exec((client, {code}, reply, ev) => { 80 | if (engine.getBackend() === "discord") { 81 | const match = code.match(codeBlockPattern) 82 | if (match) code = match.groups.code 83 | const res = evaluate(code, ev, reply) 84 | const duration = `Duration: ${res.duration}ms` 85 | if (res.error) return reply(`Error:\n${format.code(res.error.stack)}\n${duration}`) 86 | if (res.data !== null) { 87 | if (res.data === '') return reply(`Empty string returned.\n${duration}`) 88 | let msg = `${format.code(res.data)}\nType: ${typeof res.data}, ${duration}` 89 | if (msg.length >= 2000) { 90 | reply(`Data is too long to post, see log.\n${duration}`) 91 | engine.log(res.data) 92 | return; 93 | } 94 | return reply(msg) 95 | } 96 | reply(`No data returned.\n${duration}`) 97 | } else { 98 | const res = evaluate(code, ev, reply) 99 | const duration = `Duration: ${res.duration}ms` 100 | if (res.error) return reply(`Error:\n${res.error.stack}\n${duration}`) 101 | if (res.data) reply(`${res.data}\n${duration}`) 102 | } 103 | }) 104 | }) 105 | }) -------------------------------------------------------------------------------- /group_list.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Forum: https://forum.sinusbot.com/resources/group-list.388/ 3 | * GitHub: https://github.com/irgendwr/sinusbot-scripts 4 | */ 5 | 6 | registerPlugin({ 7 | name: 'Group List', 8 | version: '1.1.0', 9 | description: 'List the servers groups and their IDs with the `!groups` command.', 10 | author: 'Jonas Bögle (irgendwr)', 11 | backends: ['ts3', 'discord'], 12 | vars: [{ 13 | name: 'admins', 14 | title: 'UIDs of users which have access to the command', 15 | type: 'strings', 16 | default: [] 17 | }] 18 | }, (_, config, meta) => { 19 | const event = require('event') 20 | const engine = require('engine') 21 | const backend = require('backend') 22 | const format = require('format') 23 | 24 | engine.log(`Loaded ${meta.name} v${meta.version} by ${meta.author}.`) 25 | 26 | event.on('load', () => { 27 | const command = require('command') 28 | if (!command) { 29 | engine.log('command.js library not found! Please download command.js to your scripts folder and restart the SinusBot, otherwise this script will not work.'); 30 | engine.log('command.js can be found here: https://github.com/Multivit4min/Sinusbot-Command/blob/master/command.js'); 31 | return; 32 | } 33 | 34 | command.createCommand('groups') 35 | .alias('grouplist') 36 | .help('Lists the servers groups and their IDs') 37 | .manual('Lists the servers groups and their IDs') 38 | .checkPermission(allowAdminCommands) 39 | .addArgument(command.createArgument('string').setName('name').optional()) 40 | .exec((/** @type {Client} */client, /** @type {object} */args, /** @type {(msg:string)=>void} */reply) => { 41 | let resp = format.bold('Groups:') 42 | if (args.name && args.name !== '') { 43 | // TODO: split into multiple messages if too long 44 | backend.getServerGroups().forEach(group => { 45 | if (group.name().includes(args.name)) { 46 | resp += '\n * `' + group.name() + '`, ID: `' + group.id() + '`' 47 | } 48 | }) 49 | } else { 50 | // TODO: split into multiple messages if too long 51 | backend.getServerGroups().forEach(group => { 52 | resp += '\n * `' + group.name() + '`, ID: `' + group.id() + '`' 53 | }) 54 | } 55 | 56 | reply(resp) 57 | }) 58 | }) 59 | 60 | /** 61 | * Checks if a client is allowed to use admin commands. 62 | * @param {Client} client 63 | * @returns {boolean} 64 | */ 65 | function allowAdminCommands(client) { 66 | switch (engine.getBackend()) { 67 | case "discord": 68 | return config.admins.includes(client.uid().split("/")[1]) 69 | case "ts3": 70 | return config.admins.includes(client.uid()) 71 | default: 72 | throw new Error(`Unknown backend ${engine.getBackend()}`) 73 | } 74 | } 75 | }) -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "target": "es2018", 5 | "types" : ["sinusbot-scripting-engine"] 6 | }, 7 | "typeAcquisition": {"enable": false, "include": ["sinusbot-scripting-engine"]}, 8 | "exclude": [ 9 | "node_modules", 10 | "**/node_modules/*" 11 | ] 12 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sinusbot-scripts", 3 | "version": "1.0.0", 4 | "description": "Scripts for the sinusbot.com musicbot.", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/irgendwr/sinusbot-scripts.git" 8 | }, 9 | "keywords": [ 10 | "sinusbot" 11 | ], 12 | "author": "Jonas Bögle", 13 | "bugs": { 14 | "url": "https://github.com/irgendwr/sinusbot-scripts/issues" 15 | }, 16 | "homepage": "https://github.com/irgendwr/sinusbot-scripts", 17 | "devDependencies": { 18 | "sinusbot-scripting-engine": "^1.0.18" 19 | }, 20 | "dependencies": {}, 21 | "license": "MIT" 22 | } 23 | -------------------------------------------------------------------------------- /stream2icecast.js: -------------------------------------------------------------------------------- 1 | registerPlugin({ 2 | name: 'stream2icecast', 3 | version: '1.0.1', 4 | description: 'Streams the bots audio to an icecast server.', 5 | author: 'SinusBot Team', 6 | backends: ['ts3', 'discord'], 7 | vars: [ 8 | // **DO NOT EDIT THIS HERE** 9 | // Restart the sinusbot, then enable and configure it in the webinterface. 10 | { 11 | name: 'streamServer', 12 | title: 'StreamServer URL', 13 | type: 'string', 14 | placeholder: 'http://something.example.com:8000/example' 15 | }, 16 | { 17 | name: 'streamUser', 18 | title: 'User', 19 | type: 'string' 20 | }, 21 | { 22 | name: 'streamPassword', 23 | title: 'Password', 24 | type: 'password' 25 | } 26 | ] 27 | }, (_, config, meta) => { 28 | const engine = require('engine'); 29 | const audio = require('audio'); 30 | 31 | engine.log(`Loaded ${meta.name} v${meta.version} by ${meta.author}.`); 32 | 33 | if (!config.streamUser) { 34 | config.streamUser = 'source'; 35 | } 36 | 37 | if (config.streamServer && config.streamPassword) { 38 | audio.streamToServer(config.streamServer, config.streamUser, config.streamPassword); 39 | } else { 40 | engine.log('URL or Password missing!'); 41 | } 42 | }); -------------------------------------------------------------------------------- /telegram_bot.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Forum: 3 | * GitHub: https://github.com/irgendwr/sinusbot-scripts 4 | * 5 | * @author Jonas Bögle 6 | * @license MIT 7 | * 8 | * MIT License 9 | * 10 | * Copyright (c) 2020 Jonas Bögle 11 | * 12 | * Permission is hereby granted, free of charge, to any person obtaining a copy 13 | * of this software and associated documentation files (the "Software"), to deal 14 | * in the Software without restriction, including without limitation the rights 15 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | * copies of the Software, and to permit persons to whom the Software is 17 | * furnished to do so, subject to the following conditions: 18 | * 19 | * The above copyright notice and this permission notice shall be included in all 20 | * copies or substantial portions of the Software. 21 | * 22 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 28 | * SOFTWARE. 29 | * 30 | * Thanks to Philipp (pr31337) for donating, suggesting the idea and keeping it open source! 31 | * 32 | * Thanks to the following GitHub sponsors for supporting my work: 33 | * - Michael Friese 34 | * - Jay Vasallo 35 | * 36 | * https://github.com/sponsors/irgendwr 37 | */ 38 | registerPlugin({ 39 | name: 'Telegram Bot', 40 | version: '1.0.0', 41 | description: 'Responds to Telegram commands like `/status` and `/playing`.', 42 | author: 'Jonas Bögle (@irgendwr)', 43 | engine: '>= 1.0.0', 44 | backends: ['ts3', 'discord'], 45 | requiredModules: ['http'], 46 | vars: [ 47 | // { 48 | // name: 'master', 49 | // title: 'Main Instance (only enable this once per Telegram bot!)', 50 | // type: 'checkbox', 51 | // default: true, 52 | // }, 53 | { 54 | name: 'message', 55 | title: 'Message (placeholders: channellist, clientcount, userid, chatid, username, first_name, last_name)', 56 | type: 'multiline', 57 | placeholder: `Example:\n{clientcount} clients online:\n{channellist}`, 58 | default: `{clientcount} clients online:\n{channellist}`, 59 | }, 60 | { 61 | name: 'help', 62 | title: '/help Message', 63 | type: 'multiline', 64 | default: `Commands: 65 | /help - Shows this message 66 | /status - Shows clients 67 | /playing - Shows currently playing song 68 | /about - About this bot`, 69 | // conditions: [{ field: 'master', value: true }], 70 | }, 71 | { 72 | name: 'token', 73 | title: 'Telegram Bot Token from @BotFather (https://t.me/BotFather)', 74 | type: 'password', 75 | placeholder: '', 76 | // conditions: [{ field: 'master', value: true }], 77 | }, 78 | ] 79 | }, (_, config, meta) => { 80 | const engine = require('engine') 81 | const event = require('event') 82 | const backend = require('backend') 83 | const http = require('http') 84 | const store = require('store') 85 | const audio = require('audio') 86 | const media = require('media') 87 | 88 | // const MASTER = config.master; 89 | const TOKEN = config.token; 90 | 91 | engine.log(`Loaded ${meta.name} v${meta.version} by ${meta.author}.`); 92 | // engine.log(`master=${config.master}`); 93 | 94 | // // NON-MASTER SECTION 95 | // if (!MASTER) { 96 | // event.on('telegramstatus', ev => { 97 | // engine.log('Status event!'); 98 | // // ev.respond(format(config.message, {msg: ev.msg}, placeholders)); 99 | // }) 100 | // return; 101 | // } 102 | 103 | // check if bot token is set 104 | if (!TOKEN) { 105 | engine.log('Please set the Telegram Bot Token in the script config.'); 106 | engine.notify('Please set the Telegram Bot Token in the script config.'); 107 | return; 108 | } 109 | 110 | const CLIENT_PREFIX = " "; 111 | // Maximum retries after which to top querying. 112 | const MAX_RETRIES = 5; 113 | // Interval in which to check for new messages. 114 | const INTERVAL = 30; // seconds 115 | // Request Timeout 116 | const TIMEOUT = INTERVAL; 117 | 118 | let bot; 119 | telegram('getMe').then(user => { bot = user; }).catch(err => { 120 | engine.log(`Error on getMe: ${err}`); 121 | }); 122 | 123 | // clear webhook 124 | telegram('setWebhook', { 125 | url: '', 126 | }).catch(err => { 127 | engine.log(`Error while clearing webhook: ${err}`); 128 | }); 129 | 130 | // error counter 131 | let errors = 0; 132 | // update offset 133 | let offset = store.getInstance('offset') || null; 134 | let intervalID; 135 | 136 | startPolling(); 137 | 138 | function startPolling() { 139 | // clear previous interval 140 | if (intervalID) stopPolling(); 141 | 142 | // start interval 143 | intervalID = setInterval(queryTelegram, INTERVAL * 1000 + 100 /* milliseconds */); 144 | 145 | // poll now 146 | queryTelegram(); 147 | } 148 | 149 | function stopPolling() { 150 | clearInterval(intervalID); 151 | } 152 | 153 | function queryTelegram() { 154 | telegram('getUpdates', { 155 | timeout: TIMEOUT+2, // long polling 156 | allowed_updates: ['message'], // only update on messages 157 | offset: offset, // start after last update 158 | }).then(updates => { 159 | // set offset, so we don't read the same messages multiple times 160 | offset = updates[updates.length-1].update_id + 1; 161 | 162 | updates.forEach(update => { 163 | const msg = update.message; 164 | // ignore non-message updates 165 | if (!msg) return engine.log(`ignoring non-message update: ${update}`); 166 | 167 | handleMessage(msg); 168 | }); 169 | 170 | // reset error counter 171 | errors = 0; 172 | 173 | // update offset in storage 174 | store.setInstance('offset', offset); 175 | 176 | // restart polling 177 | startPolling(); 178 | }) 179 | .catch(err => { 180 | // ignore timeouts since we are using long polling 181 | if (typeof err === 'string' && err.includes('Timeout exceeded while awaiting headers')) return; 182 | 183 | if (err === 'Unexpected status code: 409') { 184 | engine.log('Polling Error. This happens when you enable the script more than once.'); 185 | // ...or if anyone messes up the TIMEOUT(s) or webhook is still set 186 | } else { 187 | engine.log(err); 188 | } 189 | 190 | errors++; 191 | 192 | if (errors > MAX_RETRIES) { 193 | engine.log('Aborting due to too many Telegram API errors. Please restart the script by pressing "Save Changes" in the script settings.'); 194 | stopPolling(); 195 | } 196 | }); 197 | } 198 | 199 | function handleMessage(msg) { 200 | let text = msg.text || ''; 201 | 202 | engine.log(`received '${text}' from ${msg.chat.id} (${msg.chat.type})`); 203 | 204 | text = text.replace(`@${bot.username}`, ''); 205 | 206 | switch (msg.text) { 207 | case '/start': 208 | case '/help': 209 | handleHelp(msg); 210 | break; 211 | case '/playing': 212 | handlePlaying(msg); 213 | break; 214 | case '/online': 215 | case '/status': 216 | handleStatus(msg); 217 | break; 218 | case '/about': 219 | handleAbout(msg); 220 | break; 221 | } 222 | } 223 | 224 | const placeholders = { 225 | userid: ctx => ctx.msg.from.id || '', 226 | chatid: ctx => ctx.msg.chat.id || '', 227 | username: ctx => ctx.msg.from.username || '', 228 | first_name: ctx => ctx.msg.from.first_name || '', 229 | last_name: ctx => ctx.msg.from.last_name || '', 230 | clientcount: () => backend.getChannels().reduce((sum, c) => sum + c.getClientCount(), 0), 231 | channellist: () => { 232 | let list = ''; 233 | 234 | backend.getChannels() 235 | .sort((a, b) => a.position() - b.position()) 236 | .forEach(channel => { 237 | let clients = channel.getClients(); 238 | // ignore empty channels 239 | if (!clients || clients.length === 0) return; 240 | 241 | list += `${channel.name()}:`; 242 | clients.sort().forEach(client => { 243 | list += `\n${CLIENT_PREFIX}${client.name()}` 244 | }); 245 | list += '\n'; 246 | }); 247 | 248 | return list; 249 | }, 250 | } 251 | 252 | function handleHelp(msg) { 253 | sendMessage(msg.chat.id, config.help) 254 | .catch(err => { 255 | engine.log(err); 256 | }); 257 | } 258 | 259 | function handlePlaying(msg) { 260 | let response = 'There is nothing playing at the moment.'; 261 | if (audio.isPlaying()) { 262 | response = formatTrack(media.getCurrentTrack()); 263 | } 264 | 265 | sendMessage(msg.chat.id, response) 266 | .catch(err => { 267 | engine.log(err); 268 | }); 269 | } 270 | 271 | function handleStatus(msg) { 272 | sendMessage(msg.chat.id, format(config.message, {msg: msg}, placeholders)) 273 | .catch(err => { 274 | engine.log(err); 275 | }); 276 | 277 | // this doesn't work due to a bug in the SinusBot :( 278 | 279 | // try { 280 | // event.broadcast('telegramstatus', { 281 | // msg: msg, 282 | // respond: text => { 283 | // sendMessage(msg.chat.id, text) 284 | // .catch(err => { 285 | // engine.log(err); 286 | // }); 287 | // }, 288 | // }); 289 | // } catch (error) { 290 | // engine.log(`Error on broadcast: ${error}`); 291 | // } 292 | } 293 | 294 | function handleAbout(msg) { 295 | sendMessage(msg.chat.id, 'This bot was developed by Jonas Bögle. Check out the code on GitHub.', 'HTML') 296 | .catch(err => { 297 | engine.log(err); 298 | }); 299 | } 300 | 301 | /** 302 | * Sends a Telegram text message. 303 | * @param {(string|number)} chat Chat ID 304 | * @param {string} text Message Text 305 | * @param {string} [mode] Parse Mode (Markdown/HTML) 306 | */ 307 | function sendMessage(chat, text, mode=null) { 308 | let data = { 309 | 'chat_id': chat, 310 | 'text': text, 311 | }; 312 | 313 | if (mode) data.parse_mode = mode; 314 | 315 | return telegram('sendMessage', data); 316 | } 317 | 318 | /** 319 | * Executes a Telegram API call 320 | * @param {string} method Telegram API Method 321 | * @param {object} [data] json data 322 | * @return {Promise} 323 | * @author Jonas Bögle 324 | * @license MIT 325 | */ 326 | function telegram(method, data=null) { 327 | return new Promise((resolve, reject) => { 328 | http.simpleRequest({ 329 | method: 'POST', 330 | url: `https://api.telegram.org/bot${TOKEN}/${method}`, 331 | timeout: TIMEOUT * 1000, 332 | body: data != null ? JSON.stringify(data) : data, 333 | headers: { 334 | 'Content-Type': 'application/json' 335 | } 336 | }, (error, response) => { 337 | // check for lower level request errors 338 | if (error) { 339 | return reject(error); 340 | } 341 | 342 | // check if status code is OK (200) 343 | if (response.statusCode != 200) { 344 | switch (response.statusCode) { 345 | case 400: reject('Bad Request. Either an invalid configuration or error in the code.'); break; 346 | case 401: reject('Invalid Bot Token'); break; 347 | case 404: reject('Invalid Bot Token or Method'); break; 348 | default: reject(`Unexpected status code: ${response.statusCode}`); 349 | } 350 | return; 351 | } 352 | 353 | // parse JSON response 354 | var res; 355 | try { 356 | res = JSON.parse(response.data.toString()); 357 | } catch (err) { 358 | engine.log(err.message || err); 359 | } 360 | 361 | // check if parsing was successfull 362 | if (res === undefined) { 363 | return reject("Invalid JSON"); 364 | } 365 | 366 | // check for api errors 367 | if (!res.ok) { 368 | engine.log(response.data.toString()); 369 | return reject(`API Error: ${res.description || 'unknown'}`); 370 | } 371 | 372 | // success! 373 | resolve(res.result); 374 | }); 375 | }); 376 | } 377 | 378 | /** 379 | * Formats a string with placeholders. 380 | * @param {string} str Format String 381 | * @param {*} ctx Context 382 | * @param {object} placeholders Placeholders 383 | * @author Jonas Bögle 384 | * @license MIT 385 | */ 386 | function format(str, ctx, placeholders) { 387 | return str.replace(/((?:[^{}]|(?:\{\{)|(?:\}\}))+)|(?:\{([0-9_\-a-zA-Z]+)(?:\((.+)\))?\})/g, (m, str, placeholder, argsStr) => { 388 | if (str) { 389 | return str.replace(/(?:{{)|(?:}})/g, m => m[0]); 390 | } else { 391 | if (!placeholders.hasOwnProperty(placeholder) || typeof placeholders[placeholder] !== 'function') { 392 | engine.log(`Unknown placeholder: ${placeholder}`); 393 | return `{${placeholder}: unknown placeholder}`; 394 | } 395 | let args = []; 396 | if (argsStr && argsStr.length > 0) { 397 | args = argsStr.split(/\s*(? arg.replace('\\,', ',')); 399 | } 400 | 401 | let result = `{${placeholder}: empty}`; 402 | try { 403 | result = placeholders[placeholder](ctx, ...args); 404 | } catch(ex) { 405 | result = `{${placeholder}: error}`; 406 | engine.log(`placeholder "${placeholder}" caused an error: ${ex}`); 407 | } 408 | 409 | return result; 410 | } 411 | }); 412 | } 413 | 414 | /** 415 | * Returns a formatted string from a track. 416 | * 417 | * @param {Track} track 418 | * @returns {string} formatted string 419 | * @author Jonas Bögle 420 | * @license MIT 421 | */ 422 | function formatTrack(track) { 423 | let title = track.tempTitle() || track.title(); 424 | let artist = track.tempArtist() || track.artist(); 425 | return artist ? `${artist} - ${title}` : title; 426 | } 427 | }); 428 | -------------------------------------------------------------------------------- /uptimerobot.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Forum: https://forum.sinusbot.com/resources/uptimerobot.127/ 3 | * GitHub: https://github.com/irgendwr/sinusbot-scripts 4 | */ 5 | 6 | registerPlugin({ 7 | name: 'Uptimerobot - Server Status/Uptime Monitoring', 8 | version: '3.0.2', 9 | description: 'Informs you about the status of a server configured on uptimerobot.com', 10 | author: 'Jonas Bögle (irgendwr)', 11 | engine: '>= 1.0.0', 12 | backends: ['ts3'], 13 | requiredModules: ['http'], 14 | vars: [ 15 | { 16 | name: 'info', 17 | title: 'Placeholders: \n%name%, %uptime%, %url%, %port%, %type%, %status%, %id%, %created%, %avg_response_time%, %last_response_time%, %ssl.brand%, %ssl.product%, %ssl.expires%' 18 | }, 19 | { 20 | name: 'servers', 21 | title: 'Servers', 22 | type: 'array', 23 | vars: [ 24 | { 25 | name: 'channelEnabled', 26 | title: 'Show status in channel', 27 | type: 'checkbox' 28 | }, 29 | { 30 | name: 'channel', 31 | title: 'Channel', 32 | type: 'channel', 33 | indent: 3, 34 | conditions: [{ field: 'channelEnabled', value: true }] 35 | }, 36 | { 37 | name: 'channelName', 38 | title: 'Channel Name', 39 | type: 'string', 40 | placeholder: '[cspacer]%name% is %status%', 41 | default: '[cspacer]%name% is %status%', 42 | indent: 3, 43 | conditions: [{ field: 'channelEnabled', value: true }] 44 | }, 45 | { 46 | name: 'channelDescription', 47 | title: 'Channel Description', 48 | type: 'multiline', 49 | placeholder: '[SIZE=12][B]%name%[/B][/SIZE]\n[B]Status[/B]: %status%\n[B]Uptime[/B]: %uptime%', 50 | default: '[SIZE=12][B]%name%[/B][/SIZE]\n[B]Status[/B]: %status%\n[B]Uptime[/B]: %uptime%', 51 | indent: 3, 52 | conditions: [{ field: 'channelEnabled', value: true }] 53 | }, 54 | { 55 | name: 'chatEnabled', 56 | title: 'Send a server-message if a servers status changes', 57 | type: 'checkbox' 58 | }, 59 | { 60 | name: 'chatMessage', 61 | title: 'Message', 62 | type: 'string', 63 | placeholder: '[B]%name%[/B] is [B]%status%[/B]', 64 | default: '[B]%name%[/B] is [B]%status%[/B]', 65 | indent: 3, 66 | conditions: [{ field: 'chatEnabled', value: true }] 67 | }, 68 | { 69 | name: 'apikey', 70 | title: 'Monitor-specific API key', 71 | type: 'string' 72 | } 73 | ] 74 | }, 75 | { 76 | name: 'interval', 77 | title: 'Refresh interval (in seconds)', 78 | type: 'number', 79 | placeholder: '60', 80 | default: 60 81 | }, 82 | { 83 | name: 'customText', 84 | title: 'Show/hide custom text options', 85 | type: 'checkbox' 86 | }, 87 | { 88 | name: 'textUp', 89 | title: '"up"', 90 | type: 'string', 91 | placeholder: 'up', 92 | default: 'up', 93 | conditions: [{ field: 'customText', value: true }] 94 | }, 95 | { 96 | name: 'textDown', 97 | title: '"down"', 98 | type: 'string', 99 | placeholder: 'down', 100 | default: 'down', 101 | conditions: [{ field: 'customText', value: true }] 102 | }, 103 | { 104 | name: 'textPaused', 105 | title: '"paused"', 106 | type: 'string', 107 | placeholder: 'paused', 108 | default: 'paused', 109 | conditions: [{ field: 'customText', value: true }] 110 | }, 111 | { 112 | name: 'textUnknown', 113 | title: '"unknown"', 114 | type: 'string', 115 | placeholder: 'unknown', 116 | default: 'unknown', 117 | conditions: [{ field: 'customText', value: true }] 118 | } 119 | ] 120 | }, (_, config, meta) => { 121 | const engine = require('engine') 122 | const backend = require('backend') 123 | const http = require('http') 124 | 125 | let servers = config.servers 126 | if (!servers || servers.length == 0) { 127 | engine.log('No servers configured.') 128 | return 129 | } 130 | 131 | engine.log(`Loaded ${meta.name} v${meta.version} by ${meta.author}.`) 132 | 133 | // uncomment the following lines if you want to enable the status command: 134 | 135 | // const event = require('event') 136 | // event.on('load', () => { 137 | // // @ts-ignore 138 | // const command = require("command") 139 | // if (!command) return engine.log("Command.js not found! Please be sure to install and enable Command.js") 140 | // // @ts-ignore 141 | // const {createCommand, createArgument} = command 142 | 143 | // createCommand('status') 144 | // .help('Shows uptimerobot status') 145 | // .manual('Shows uptimerobot status') 146 | // //.checkPermission((/** @type {Client} */client) => client.uid() === '') 147 | // .exec((client, args, reply, /** @implements {Message} */ev) => { 148 | // servers.forEach(function (server) { 149 | // fetchData(server, function (data) { 150 | // reply(parse(server.chatMessage, data, true, 1024)) 151 | // }) 152 | // }) 153 | // }) 154 | // }) 155 | 156 | refresh() 157 | setInterval(refresh, (config.interval || 60) * 1000) 158 | 159 | function refresh() { 160 | for (let server of servers) { 161 | fetchData(server, data => { 162 | if (server.chatEnabled && server.lastStatus != data.status) { 163 | backend.chat(parse(server.chatMessage, data, true, 1024)) 164 | server.lastStatus = data.status 165 | } 166 | 167 | if (server.channelEnabled) { 168 | if (server.channel == null) { 169 | engine.log('[Error] You enabled "Show status in channel" but didn\'t set a channel.') 170 | server.channelEnabled = false 171 | return 172 | } 173 | 174 | const channel = backend.getChannelByID(server.channel) 175 | if (!channel) { 176 | engine.log(`[Error] Unable to get channel with ID: ${server.channel}`) 177 | return 178 | } 179 | 180 | channel.setName(parse(server.channelName, data, false, 40)) 181 | channel.setDescription(parse(server.channelDescription, data, true, 8192)) 182 | } 183 | }) 184 | } 185 | } 186 | 187 | /** 188 | * Fetches data from uptimerobot 189 | * @param {Object} server Server Config 190 | * @param {function} callback Callback 191 | */ 192 | function fetchData(server, callback) { 193 | var params = JSON.stringify({ 194 | format: 'json', 195 | api_key: server.apikey, 196 | all_time_uptime_ratio: 1, 197 | response_times: 1, 198 | response_times_limit: 1, 199 | response_times_average: 1 200 | }) 201 | 202 | http.simpleRequest({ 203 | method: 'POST', 204 | url: 'https://api.uptimerobot.com/v2/getMonitors', 205 | timeout: 6000, 206 | body: params, 207 | headers: { 208 | 'Content-Type': 'application/json' 209 | } 210 | }, (error, response) => { 211 | // check whether request was successfull 212 | if (error || response.statusCode != 200) { 213 | engine.log(`[Error] API request failed: ${(error || 'HTTP '+response.statusCode)}`) 214 | return 215 | } 216 | 217 | var data 218 | try { 219 | data = JSON.parse(response.data.toString()) 220 | } catch (err) { 221 | engine.log(`[Error] Unable to parse data: ${err}`) 222 | engine.log(`Response: ${response.data}`) 223 | } 224 | 225 | // check whether response is valid 226 | if (!data) { 227 | return 228 | } else if (data.stat == 'fail') { 229 | engine.log(`[Error] API Request failed: ${JSON.stringify(data.error)}`) 230 | return 231 | } 232 | 233 | // engine.log(`Data: ${response.data}`) 234 | 235 | data = data.monitors[0] 236 | callback(data) 237 | }) 238 | } 239 | 240 | /** 241 | * Replaces placeholders and limits string lenght 242 | * @param {string} str String 243 | * @param {Object} data Data 244 | * @param {boolean} fmt Set to true if string should be formatted 245 | * @param {number} len Max. length of the string 246 | * @returns {string} 247 | */ 248 | function parse(str, data, fmt, len) { 249 | return trunc(replacePlaceholders( 250 | str, data, fmt 251 | ), len) 252 | } 253 | 254 | const types = [ 255 | '', 256 | 'HTTP(s)', 257 | 'Keyword', 258 | 'Ping', 259 | 'Port' 260 | ] 261 | const UP = config.textUp || 'up'; 262 | const DOWN = config.textDown || 'down'; 263 | const PAUSED = config.textPaused || 'paused'; 264 | const UNKNOWN = config.textUnknown || 'unknown'; 265 | 266 | /** 267 | * Replaces placeholders 268 | * @param {string} str String 269 | * @param {Object} data Data 270 | * @param {boolean} fmt Set to true if string should be formatted 271 | * @returns {string} 272 | */ 273 | function replacePlaceholders(str, data, fmt) { 274 | if (!str || !data) return ''; 275 | 276 | let status = [ 277 | fmt ? `[color=#000000]${PAUSED}[/color]` : PAUSED, 278 | fmt ? `[color=#464646]${UNKNOWN}[/color]` : UNKNOWN, 279 | fmt ? `[color=#4da74d]${UP}[/color]` : UP, 280 | ] 281 | status[9] = fmt ? `[color=#ff2121]${DOWN}[/color]` : DOWN 282 | status[8] = status[9] // "seems down" 283 | 284 | str = str.replace(/%name%/gi, data.friendly_name) 285 | .replace(/%uptime%/gi, data.all_time_uptime_ratio + '%') 286 | .replace(/%(url|ip)%/gi, data.url) 287 | .replace(/%port%/gi, data.port) 288 | .replace(/%type%/gi, types[data.type]) 289 | .replace(/%status%/gi, status[data.status]) 290 | .replace(/%id%/gi, data.id) 291 | .replace(/%ssl\.brand%/gi, data.ssl && data.ssl.brand ? data.ssl.brand : '') 292 | .replace(/%ssl\.product%/gi, data.ssl && data.ssl.product ? data.ssl.product : '' || '') 293 | .replace(/%last_response_time%/gi, data.response_times && data.response_times.length == 1 ? data.response_times[0].value + 'ms' : '') 294 | .replace(/%avg_response_time%/gi, data.average_response_time ? data.average_response_time + 'ms' : '') 295 | 296 | // don't use Date() with sinusbot alpha 6 or lower due to bug 297 | if (engine.version() > '1.0.0-alpha' && engine.version() < '1.0.0-alpha.7') { 298 | str = str.replace(/%ssl\.expires%/gi, '').replace(/%created%/gi, '') 299 | } else { 300 | str = str.replace(/%ssl\.expires%/gi, data.ssl && data.ssl.expires ? new Date(data.ssl.expires * 1000).toLocaleString() : '') 301 | .replace(/%created%/gi, new Date(data.create_datetime * 1000).toLocaleString()) 302 | } 303 | 304 | return str 305 | } 306 | 307 | /** 308 | * Truncates a string to a specified length 309 | * @param {string} str String 310 | * @param {number} len Max. length of the string 311 | * @returns {string} 312 | */ 313 | function trunc(str, len) { 314 | return (str.length > len) ? str.substr(0, len - 1) + '…' : str 315 | } 316 | }) --------------------------------------------------------------------------------