diff --git a/Discord.Net b/Discord.Net index fa2568bc..b9f76733 160000 --- a/Discord.Net +++ b/Discord.Net @@ -1 +1 @@ -Subproject commit fa2568bc312ba35f1518e47601c62fccdb949731 +Subproject commit b9f767337d2b7c07ed76eb83c3bc5030109d5238 diff --git a/NadekoBot.sln b/NadekoBot.sln index e0ebea17..7551e778 100644 --- a/NadekoBot.sln +++ b/NadekoBot.sln @@ -12,8 +12,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "NadekoBot", "src\NadekoBot\NadekoBot.xproj", "{45EC1473-C678-4857-A544-07DFE0D0B478}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net.API", "discord.net\src\Discord.Net.API\Discord.Net.API.xproj", "{834C70DF-1230-4AAA-9C13-48AB232E8D76}" -EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net.Commands", "discord.net\src\Discord.Net.Commands\Discord.Net.Commands.xproj", "{078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}" EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net.Core", "discord.net\src\Discord.Net.Core\Discord.Net.Core.xproj", "{E5F4786F-58F3-469E-8C87-1908A95436B7}" @@ -35,12 +33,6 @@ Global {45EC1473-C678-4857-A544-07DFE0D0B478}.GlobalNadeko|Any CPU.Build.0 = Release|Any CPU {45EC1473-C678-4857-A544-07DFE0D0B478}.Release|Any CPU.ActiveCfg = Release|Any CPU {45EC1473-C678-4857-A544-07DFE0D0B478}.Release|Any CPU.Build.0 = Release|Any CPU - {834C70DF-1230-4AAA-9C13-48AB232E8D76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {834C70DF-1230-4AAA-9C13-48AB232E8D76}.Debug|Any CPU.Build.0 = Debug|Any CPU - {834C70DF-1230-4AAA-9C13-48AB232E8D76}.GlobalNadeko|Any CPU.ActiveCfg = Debug|Any CPU - {834C70DF-1230-4AAA-9C13-48AB232E8D76}.GlobalNadeko|Any CPU.Build.0 = Debug|Any CPU - {834C70DF-1230-4AAA-9C13-48AB232E8D76}.Release|Any CPU.ActiveCfg = Release|Any CPU - {834C70DF-1230-4AAA-9C13-48AB232E8D76}.Release|Any CPU.Build.0 = Release|Any CPU {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Debug|Any CPU.Build.0 = Debug|Any CPU {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.GlobalNadeko|Any CPU.ActiveCfg = Debug|Any CPU diff --git a/docs/Commands List.md b/docs/Commands List.md index 68e59981..049143f8 100644 --- a/docs/Commands List.md +++ b/docs/Commands List.md @@ -18,8 +18,63 @@ You can support the project on patreon: or paypa ### Administration Command and aliases | Description | Usage ----------------|--------------|------- -`.resetperms` | Resets BOT's permissions module on this server to the default value. **Requires Administrator server permission.** | `.resetperms` -`.delmsgoncmd` | Toggles the automatic deletion of user's successful command message to prevent chat flood. **Requires Administrator server permission.** | `.delmsgoncmd` +`.voice+text` `.v+t` | Creates a text channel for each voice channel only users in that voice channel can see.If you are server owner, keep in mind you will see them all the time regardless. **Requires ManageRoles server permission.** **Requires ManageChannels server permission.** | `.voice+text` +`.cleanvplust` `.cv+t` | Deletes all text channels ending in `-voice` for which voicechannels are not found. Use at your own risk. **Requires ManageChannels server permission.** **Requires ManageRoles server permission.** | `.cleanv+t` +`.greetdel` `.grdel` | Sets the time it takes (in seconds) for greet messages to be auto-deleted. Set 0 to disable automatic deletion. **Requires ManageServer server permission.** | `.greetdel 0` or `.greetdel 30` +`.greet` | Toggles anouncements on the current channel when someone joins the server. **Requires ManageServer server permission.** | `.greet` +`.greetmsg` | Sets a new join announcement message which will be shown in the server's channel. Type %user% if you want to mention the new member. Using it with no message will show the current greet message. **Requires ManageServer server permission.** | `.greetmsg Welcome, %user%.` +`.greetdm` | Toggles whether the greet messages will be sent in a DM (This is separate from greet - you can have both, any or neither enabled). **Requires ManageServer server permission.** | `.greetdm` +`.greetdmmsg` | Sets a new join announcement message which will be sent to the user who joined. Type %user% if you want to mention the new member. Using it with no message will show the current DM greet message. **Requires ManageServer server permission.** | `.greetdmmsg Welcome to the server, %user%`. +`.bye` | Toggles anouncements on the current channel when someone leaves the server. **Requires ManageServer server permission.** | `.bye` +`.byemsg` | Sets a new leave announcement message. Type %user% if you want to show the name the user who left. Type %id% to show id. Using this command with no message will show the current bye message. **Requires ManageServer server permission.** | `.byemsg %user% has left.` +`.byedel` | Sets the time it takes (in seconds) for bye messages to be auto-deleted. Set 0 to disable automatic deletion. **Requires ManageServer server permission.** | `.byedel 0` or `.byedel 30` +`.leave` | Makes Nadeko leave the server. Either name or id required. **Bot Owner only.** | `.leave 123123123331` +`.die` | Shuts the bot down. **Bot Owner only.** | `.die` +`.setname` `.newnm` | Gives the bot a new name. **Bot Owner only.** | `.newnm BotName` +`.setstatus` | Sets the bot's status. (Online/Idle/Dnd/Invisible) **Bot Owner only.** | `.setstatus Idle` +`.setavatar` `.setav` | Sets a new avatar image for the NadekoBot. Argument is a direct link to an image. **Bot Owner only.** | `.setav http://i.imgur.com/xTG3a1I.jpg` +`.setgame` | Sets the bots game. **Bot Owner only.** | `.setgame with snakes` +`.setstream` | Sets the bots stream. First argument is the twitch link, second argument is stream name. **Bot Owner only.** | `.setstream TWITCHLINK Hello` +`.send` | Sends a message to someone on a different server through the bot. Separate server and channel/user ids with `|` and prepend channel id with `c:` and user id with `u:`. **Bot Owner only.** | `.send serverid|c:channelid message` or `.send serverid|u:userid message` +`.announce` | Sends a message to all servers' general channel bot is connected to. **Bot Owner only.** | `.announce Useless spam` +`.adsarm` | Toggles the automatic deletion of confirmations for .iam and .iamn commands. **Requires ManageMessages server permission.** | `.adsarm` +`.asar` | Adds a role to the list of self-assignable roles. **Requires ManageRoles server permission.** | `.asar Gamer` +`.rsar` | Removes a specified role from the list of self-assignable roles. **Requires ManageRoles server permission.** | `.rsar` +`.lsar` | Lists all self-assignable roles. | `.lsar` +`.togglexclsar` `.tesar` | Toggles whether the self-assigned roles are exclusive. (So that any person can have only one of the self assignable roles) **Requires ManageRoles server permission.** | `.tesar` +`.iam` | Adds a role to you that you choose. Role must be on a list of self-assignable roles. | `.iam Gamer` +`.iamnot` `.iamn` | Removes a role to you that you choose. Role must be on a list of self-assignable roles. | `.iamn Gamer` +`.slowmode` | Toggles slowmode. Disable by specifying no parameters. To enable, specify a number of messages each user can send, and an interval in seconds. For example 1 message every 5 seconds. **Requires ManageMessages server permission.** | `.slowmode 1 5` or `.slowmode` +`.rotateplaying` `.ropl` | Toggles rotation of playing status of the dynamic strings you previously specified. **Bot Owner only.** | `.ropl` +`.addplaying` `.adpl` | Adds a specified string to the list of playing strings to rotate. Supported placeholders: %servers%, %users%, %playing%, %queued% **Bot Owner only.** | `.adpl` +`.listplaying` `.lipl` | Lists all playing statuses with their corresponding number. **Bot Owner only.** | `.lipl` +`.removeplaying` `.rmpl` `.repl` | Removes a playing string on a given number. **Bot Owner only.** | `.rmpl` +`.setmuterole` | Sets a name of the role which will be assigned to people who should be muted. Default is nadeko-mute. **Requires ManageRoles server permission.** | `.setmuterole Silenced` +`.mute` | Mutes a mentioned user both from speaking and chatting. **Requires ManageRoles server permission.** **Requires MuteMembers server permission.** | `.mute @Someone` +`.unmute` | Unmutes a mentioned user previously muted with `.mute` command. **Requires ManageRoles server permission.** **Requires MuteMembers server permission.** | `.unmute @Someone` +`.chatmute` | Prevents a mentioned user from chatting in text channels. **Requires ManageRoles server permission.** | `.chatmute @Someone` +`.chatunmute` | Removes a mute role previously set on a mentioned user with `.chatmute` which prevented him from chatting in text channels. **Requires ManageRoles server permission.** | `.chatunmute @Someone` +`.voicemute` | Prevents a mentioned user from speaking in voice channels. **Requires MuteMembers server permission.** | `.voicemute @Someone` +`.voiceunmute` | Gives a previously voice-muted user a permission to speak. **Requires MuteMembers server permission.** | `.voiceunmute @Someguy` +`.migratedata` | Migrate data from old bot configuration **Bot Owner only.** | `.migratedata` +`.repeatinvoke` `.repinv` | Immediately shows the repeat message on a certain index and restarts its timer. **Requires ManageMessages server permission.** | `.repinv 1` +`.repeatremove` `.reprm` | Removes a repeating message on a specified index. Use `.repeatlist` to see indexes. **Requires ManageMessages server permission.** | `.reprm 2` +`.repeat` | Repeat a message every X minutes in the current channel. **Requires ManageMessages server permission.** | `.repeat 5 Hello there` +`.repeatlist` `.replst` | Shows currently repeating messages and their indexes. **Requires ManageMessages server permission.** | `.repeatlist` +`.logserver` | Enables or Disables ALL log events. If enabled, all log events will log to this channel. **Requires Administrator server permission.** **Bot Owner only.** | `.logserver enable` or `.logserver disable` +`.logignore` | Toggles whether the .logserver command ignores this channel. Useful if you have hidden admin channel and public log channel. **Requires Administrator server permission.** **Bot Owner only.** | `.logignore` +`.logevents` | Shows a list of all events you can subscribe to with `.log` **Requires Administrator server permission.** **Bot Owner only.** | `.logevents` +`.log` | Toggles logging event. Disables it if it's active anywhere on the server. Enables if it's not active. Use `.logevents` to see a list of all events you can subscribe to. **Requires Administrator server permission.** **Bot Owner only.** | `.log userpresence` or `.log userbanned` +`.fwmsgs` | Toggles forwarding of non-command messages sent to bot's DM to the bot owners **Bot Owner only.** | `.fwmsgs` +`.fwtoall` | Toggles whether messages will be forwarded to all bot owners or only to the first one specified in the credentials.json **Bot Owner only.** | `.fwtoall` +`.scsc` | Starts an instance of cross server channel. You will get a token as a DM that other people will use to tune in to the same instance. **Bot Owner only.** | `.scsc` +`.jcsc` | Joins current channel to an instance of cross server channel using the token. **Requires ManageServer server permission.** | `.jcsc TokenHere` +`.lcsc` | Leaves Cross server channel instance from this channel. **Requires ManageServer server permission.** | `.lcsc` +`.autoassignrole` `.aar` | Automaticaly assigns a specified role to every user who joins the server. **Requires ManageRoles server permission.** | `.aar` to disable, `.aar Role Name` to enable +`.antiraid` | Sets an anti-raid protection on the server. First argument is number of people which will trigger the protection. Second one is a time interval in which that number of people needs to join in order to trigger the protection, and third argument is punishment for those people (Kick, Ban, Mute) **Requires Administrator server permission.** | `.antiraid 5 20 Kick` +`.antispam` | Stops people from repeating same message X times in a row. You can specify to either mute, kick or ban the offenders. **Requires Administrator server permission.** | `.antispam 3 Mute` or `.antispam 4 Kick` or `.antispam 6 Ban` +`.resetperms` | Resets BOT's permissions module on this server to the default value. **Requires Administrator server permission.** | `.resetperms` +`.delmsgoncmd` | Toggles the automatic deletion of user's successful command message to prevent chat flood. **Requires Administrator server permission.** | `.delmsgoncmd` `.setrole` `.sr` | Sets a role for a given user. **Requires ManageRoles server permission.** | `.sr @User Guest` `.removerole` `.rr` | Removes a role from a given user. **Requires ManageRoles server permission.** | `.rr @User Admin` `.renamerole` `.renr` | Renames a role. Roles you are renaming must be lower than bot's highest role. **Requires ManageRoles server permission.** | `.renr "First role" SecondRole` @@ -38,62 +93,10 @@ Command and aliases | Description | Usage `.settopic` `.st` | Sets a topic on the current channel. **Requires ManageChannels server permission.** | `.st My new topic` `.setchanlname` `.schn` | Changes the name of the current channel. **Requires ManageChannels server permission.** | `.schn NewName` `.prune` `.clr` | `.prune` removes all nadeko's messages in the last 100 messages.`.prune X` removes last X messages from the channel (up to 100)`.prune @Someone` removes all Someone's messages in the last 100 messages.`.prune @Someone X` removes last X 'Someone's' messages in the channel. | `.prune` or `.prune 5` or `.prune @Someone` or `.prune @Someone X` -`.die` | Shuts the bot down. **Bot Owner only.** | `.die` -`.setname` `.newnm` | Gives the bot a new name. **Bot Owner only.** | `.newnm BotName` -`.setavatar` `.setav` | Sets a new avatar image for the NadekoBot. Argument is a direct link to an image. **Bot Owner only.** | `.setav http://i.imgur.com/xTG3a1I.jpg` -`.setgame` | Sets the bots game. **Bot Owner only.** | `.setgame with snakes` -`.setstream` | Sets the bots stream. First argument is the twitch link, second argument is stream name. **Bot Owner only.** | `.setstream TWITCHLINK Hello` -`.send` | Sends a message to someone on a different server through the bot. Separate server and channel/user ids with `|` and prepend channel id with `c:` and user id with `u:`. **Bot Owner only.** | `.send serverid|c:channelid message` or `.send serverid|u:userid message` -`.announce` | Sends a message to all servers' general channel bot is connected to. **Bot Owner only.** | `.announce Useless spam` -`.savechat` | Saves a number of messages to a text file and sends it to you. **Bot Owner only.** | `.savechat 150` +`.savechat` | Saves a number of messages to a text file and sends it to you. **Bot Owner only.** | `.savechat 150` `.mentionrole` `.menro` | Mentions every person from the provided role or roles (separated by a ',') on this server. Requires you to have mention everyone permission. **Requires MentionEveryone server permission.** | `.menro RoleName` -`.donators` | List of lovely people who donated to keep this project alive. | `.donators` -`.donadd` | Add a donator to the database. **Bot Owner only.** | `.donadd Donate Amount` -`.antiraid` | Sets an anti-raid protection on the server. First argument is number of people which will trigger the protection. Second one is a time interval in which that number of people needs to join in order to trigger the protection, and third argument is punishment for those people (Kick, Ban, Mute) **Requires Administrator server permission.** | `.antiraid 5 20 Kick` -`.antispam` | Stops people from repeating same message X times in a row. You can specify to either mute, kick or ban the offenders. **Requires Administrator server permission.** | `.antispam 3 Mute` or `.antispam 4 Kick` or `.antispam 6 Ban` -`.autoassignrole` `.aar` | Automaticaly assigns a specified role to every user who joins the server. **Requires ManageRoles server permission.** | `.aar` to disable, `.aar Role Name` to enable -`.scsc` | Starts an instance of cross server channel. You will get a token as a DM that other people will use to tune in to the same instance. **Bot Owner only.** | `.scsc` -`.jcsc` | Joins current channel to an instance of cross server channel using the token. **Requires ManageServer server permission.** | `.jcsc TokenHere` -`.lcsc` | Leaves Cross server channel instance from this channel. **Requires ManageServer server permission.** | `.lcsc` -`.fwmsgs` | Toggles forwarding of non-command messages sent to bot's DM to the bot owners **Bot Owner only.** | `.fwmsgs` -`.fwtoall` | Toggles whether messages will be forwarded to all bot owners or only to the first one specified in the credentials.json **Bot Owner only.** | `.fwtoall` -`.logserver` | Enables or Disables ALL log events. If enabled, all log events will log to this channel. **Requires Administrator server permission.** **Bot Owner only.** | `.logserver enable` or `.logserver disable` -`.logignore` | Toggles whether the .logserver command ignores this channel. Useful if you have hidden admin channel and public log channel. **Requires Administrator server permission.** **Bot Owner only.** | `.logignore` -`.logevents` | Shows a list of all events you can subscribe to with `.log` **Requires Administrator server permission.** **Bot Owner only.** | `.logevents` -`.log` | Toggles logging event. Disables it if it's active anywhere on the server. Enables if it's not active. Use `.logevents` to see a list of all events you can subscribe to. **Requires Administrator server permission.** **Bot Owner only.** | `.log userpresence` or `.log userbanned` -`.repeatinvoke` `.repinv` | Immediately shows the repeat message and restarts the timer. **Requires ManageMessages server permission.** | `.repinv` -`.repeat` | Repeat a message every X minutes. If no parameters are specified, repeat is disabled. **Requires ManageMessages server permission.** | `.repeat 5 Hello there` -`.migratedata` | Migrate data from old bot configuration **Bot Owner only.** | `.migratedata` -`.setmuterole` | Sets a name of the role which will be assigned to people who should be muted. Default is nadeko-mute. **Requires ManageRoles server permission.** | `.setmuterole Silenced` -`.mute` | Mutes a mentioned user both from speaking and chatting. **Requires ManageRoles server permission.** **Requires MuteMembers server permission.** | `.mute @Someone` -`.unmute` | Unmutes a mentioned user previously muted with `.mute` command. **Requires ManageRoles server permission.** **Requires MuteMembers server permission.** | `.unmute @Someone` -`.chatmute` | Prevents a mentioned user from chatting in text channels. **Requires ManageRoles server permission.** | `.chatmute @Someone` -`.chatunmute` | Removes a mute role previously set on a mentioned user with `.chatmute` which prevented him from chatting in text channels. **Requires ManageRoles server permission.** | `.chatunmute @Someone` -`.voicemute` | Prevents a mentioned user from speaking in voice channels. **Requires MuteMembers server permission.** | `.voicemute @Someone` -`.voiceunmute` | Gives a previously voice-muted user a permission to speak. **Requires MuteMembers server permission.** | `.voiceunmute @Someguy` -`.rotateplaying` `.ropl` | Toggles rotation of playing status of the dynamic strings you previously specified. **Bot Owner only.** | `.ropl` -`.addplaying` `.adpl` | Adds a specified string to the list of playing strings to rotate. Supported placeholders: %servers%, %users%, %playing%, %queued% **Bot Owner only.** | `.adpl` -`.listplaying` `.lipl` | Lists all playing statuses with their corresponding number. **Bot Owner only.** | `.lipl` -`.removeplaying` `.rmpl` `.repl` | Removes a playing string on a given number. **Bot Owner only.** | `.rmpl` -`.slowmode` | Toggles slowmode. Disable by specifying no parameters. To enable, specify a number of messages each user can send, and an interval in seconds. For example 1 message every 5 seconds. **Requires ManageMessages server permission.** | `.slowmode 1 5` or `.slowmode` -`.adsarm` | Toggles the automatic deletion of confirmations for .iam and .iamn commands. **Requires ManageMessages server permission.** | `.adsarm` -`.asar` | Adds a role to the list of self-assignable roles. **Requires ManageRoles server permission.** | `.asar Gamer` -`.rsar` | Removes a specified role from the list of self-assignable roles. **Requires ManageRoles server permission.** | `.rsar` -`.lsar` | Lists all self-assignable roles. | `.lsar` -`.togglexclsar` `.tesar` | Toggles whether the self-assigned roles are exclusive. (So that any person can have only one of the self assignable roles) **Requires ManageRoles server permission.** | `.tesar` -`.iam` | Adds a role to you that you choose. Role must be on a list of self-assignable roles. | `.iam Gamer` -`.iamnot` `.iamn` | Removes a role to you that you choose. Role must be on a list of self-assignable roles. | `.iamn Gamer` -`.leave` | Makes Nadeko leave the server. Either name or id required. **Bot Owner only.** | `.leave 123123123331` -`.greetdel` `.grdel` | Sets the time it takes (in seconds) for greet messages to be auto-deleted. Set 0 to disable automatic deletion. **Requires ManageServer server permission.** | `.greetdel 0` or `.greetdel 30` -`.greet` | Toggles anouncements on the current channel when someone joins the server. **Requires ManageServer server permission.** | `.greet` -`.greetmsg` | Sets a new join announcement message which will be shown in the server's channel. Type %user% if you want to mention the new member. Using it with no message will show the current greet message. **Requires ManageServer server permission.** | `.greetmsg Welcome, %user%.` -`.greetdm` | Toggles whether the greet messages will be sent in a DM (This is separate from greet - you can have both, any or neither enabled). **Requires ManageServer server permission.** | `.greetdm` -`.greetdmmsg` | Sets a new join announcement message which will be sent to the user who joined. Type %user% if you want to mention the new member. Using it with no message will show the current DM greet message. **Requires ManageServer server permission.** | `.greetdmmsg Welcome to the server, %user%`. -`.bye` | Toggles anouncements on the current channel when someone leaves the server. **Requires ManageServer server permission.** | `.bye` -`.byemsg` | Sets a new leave announcement message. Type %user% if you want to show the name the user who left. Type %id% to show id. Using this command with no message will show the current bye message. **Requires ManageServer server permission.** | `.byemsg %user% has left.` -`.byedel` | Sets the time it takes (in seconds) for bye messages to be auto-deleted. Set 0 to disable automatic deletion. **Requires ManageServer server permission.** | `.byedel 0` or `.byedel 30` -`.voice+text` `.v+t` | Creates a text channel for each voice channel only users in that voice channel can see.If you are server owner, keep in mind you will see them all the time regardless. **Requires ManageRoles server permission.** **Requires ManageChannels server permission.** | `.voice+text` -`.cleanvplust` `.cv+t` | Deletes all text channels ending in `-voice` for which voicechannels are not found. Use at your own risk. **Requires ManageChannels server permission.** **Requires ManageRoles server permission.** | `.cleanv+t` +`.donators` | List of lovely people who donated to keep this project alive. | `.donators` +`.donadd` | Add a donator to the database. **Bot Owner only.** | `.donadd Donate Amount` ###### [Back to TOC](#table-of-contents) @@ -120,59 +123,60 @@ Command and aliases | Description | Usage `.listcustreactg` `.lcrg` | Lists global or server custom reactions (20 commands per page) grouped by trigger, and show a number of responses for each. Running the command in DM will list global custom reactions, while running it in server will list that server's custom reactions. | `.lcrg 1` `.showcustreact` `.scr` | Shows a custom reaction's response on a given ID. | `.scr 1` `.delcustreact` `.dcr` | Deletes a custom reaction on a specific index. If ran in DM, it is bot owner only and deletes a global custom reaction. If ran in a server, it requires Administration priviledges and removes server custom reaction. | `.dcr 5` -`.crstatsclear` | Resets the counters on `.crstats`. You can specify a trigger to clear stats only for that trigger. | `.crstatsclear` or `.crstatsclear rng` -`.crstats` | Shows a list of custom reactions and the number of times they have been executed. Paginated with 10 per page. Use `.crstatsclear` to reset the counters. | `.crstats` or `.crstats 3` +`.crstatsclear` | Resets the counters on `.crstats`. You can specify a trigger to clear stats only for that trigger. **Bot Owner only.** | `.crstatsclear` or `.crstatsclear rng` +`.crstats` | Shows a list of custom reactions and the number of times they have been executed. Paginated with 10 per page. Use `.crstatsclear` to reset the counters. | `.crstats` or `.crstats 3` ###### [Back to TOC](#table-of-contents) ### Gambling Command and aliases | Description | Usage ----------------|--------------|------- -`$raffle` | Prints a name and ID of a random user from the online list from the (optional) role. | `$raffle` or `$raffle RoleName` +`$flip` | Flips coin(s) - heads or tails, and shows an image. | `$flip` or `$flip 3` +`$betflip` `$bf` | Bet to guess will the result be heads or tails. Guessing awards you 1.8x the currency you've bet. | `$bf 5 heads` or `$bf 3 t` +`$draw` | Draws a card from the deck.If you supply number X, she draws up to 5 cards from the deck. | `$draw` or `$draw 5` +`$shuffle` `$sh` | Reshuffles all cards back into the deck. | `$sh` +`$roll` | Rolls 0-100. If you supply a number [x] it rolls up to 30 normal dice. If you split 2 numbers with letter d (xdy) it will roll x dice from 1 to y. Y can be a letter 'F' if you want to roll fate dice instead of dnd. | `$roll` or `$roll 7` or `$roll 3d5` or `$roll 5dF` +`$rolluo` | Rolls X normal dice (up to 30) unordered. If you split 2 numbers with letter d (xdy) it will roll x dice from 1 to y. | `$rolluo` or `$rolluo 7` or `$rolluo 3d5` +`$nroll` | Rolls in a given range. | `$nroll 5` (rolls 0-5) or `$nroll 5-15` +`$race` | Starts a new animal race. | `$race` +`$joinrace` `$jr` | Joins a new race. You can specify an amount of currency for betting (optional). You will get YourBet*(participants-1) back if you win. | `$jr` or `$jr 5` +`$raffle` | Prints a name and ID of a random user from the online list from the (optional) role. | `$raffle` or `$raffle RoleName` `$cash` `$$$` | Check how much currency a person has. (Defaults to yourself) | `$$$` or `$$$ @SomeGuy` -`$give` | Give someone a certain amount of currency. | `$give 1 "@SomeGuy"` -`$award` | Awards someone a certain amount of currency. You can also specify a role name to award currency to all users in a role. **Bot Owner only.** | `$award 100 @person` or `$award 5 Role Of Gamblers` -`$take` | Takes a certain amount of currency from someone. **Bot Owner only.** | `$take 1 "@someguy"` +`$give` | Give someone a certain amount of currency. | `$give 1 "@SomeGuy"` +`$award` | Awards someone a certain amount of currency. You can also specify a role name to award currency to all users in a role. **Bot Owner only.** | `$award 100 @person` or `$award 5 Role Of Gamblers` +`$take` | Takes a certain amount of currency from someone. **Bot Owner only.** | `$take 1 "@someguy"` `$betroll` `$br` | Bets a certain amount of currency and rolls a dice. Rolling over 66 yields x2 of your currency, over 90 - x3 and 100 x10. | `$br 5` `$leaderboard` `$lb` | Displays bot currency leaderboard. | `$lb` -`$race` | Starts a new animal race. | `$race` -`$joinrace` `$jr` | Joins a new race. You can specify an amount of currency for betting (optional). You will get YourBet*(participants-1) back if you win. | `$jr` or `$jr 5` -`$roll` | Rolls 0-100. If you supply a number [x] it rolls up to 30 normal dice. If you split 2 numbers with letter d (xdy) it will roll x dice from 1 to y. Y can be a letter 'F' if you want to roll fate dice instead of dnd. | `$roll` or `$roll 7` or `$roll 3d5` or `$roll 5dF` -`$rolluo` | Rolls X normal dice (up to 30) unordered. If you split 2 numbers with letter d (xdy) it will roll x dice from 1 to y. | `$rolluo` or `$rolluo 7` or `$rolluo 3d5` -`$nroll` | Rolls in a given range. | `$nroll 5` (rolls 0-5) or `$nroll 5-15` -`$draw` | Draws a card from the deck.If you supply number X, she draws up to 5 cards from the deck. | `$draw` or `$draw 5` -`$shuffle` `$sh` | Reshuffles all cards back into the deck. | `$sh` -`$flip` | Flips coin(s) - heads or tails, and shows an image. | `$flip` or `$flip 3` -`$betflip` `$bf` | Bet to guess will the result be heads or tails. Guessing awards you 1.8x the currency you've bet. | `$bf 5 heads` or `$bf 3 t` ###### [Back to TOC](#table-of-contents) ### Games Command and aliases | Description | Usage ----------------|--------------|------- -`>choose` | Chooses a thing from a list of things | `>choose Get up;Sleep;Sleep more` -`>8ball` | Ask the 8ball a yes/no question. | `>8ball should I do something` -`>rps` | Play a game of rocket paperclip scissors with Nadeko. | `>rps scissors` -`>linux` | Prints a customizable Linux interjection | `>linux Spyware Windows` -`>leet` | Converts a text to leetspeak with 6 (1-6) severity levels | `>leet 3 Hello` -`>poll` | Creates a poll which requires users to send the number of the voting option to the bot. **Requires ManageMessages server permission.** | `>poll Question?;Answer1;Answ 2;A_3` -`>publicpoll` `>ppoll` | Creates a public poll which requires users to type a number of the voting option in the channel command is ran in. **Requires ManageMessages server permission.** | `>ppoll Question?;Answer1;Answ 2;A_3` -`>pollend` | Stops active poll on this server and prints the results in this channel. **Requires ManageMessages server permission.** | `>pollend` -`>acrophobia` `>acro` | Starts an Acrophobia game. Second argment is optional round length in seconds. (default is 60) | `>acro` or `>acro 30` -`>cleverbot` | Toggles cleverbot session. When enabled, the bot will reply to messages starting with bot mention in the server. Custom reactions starting with %mention% won't work if cleverbot is enabled. **Requires ManageMessages server permission.** | `>cleverbot` -`>hangmanlist` | Shows a list of hangman term types. | `> hangmanlist` -`>hangman` | Starts a game of hangman in the channel. Use `>hangmanlist` to see a list of available term types. Defaults to 'all'. | `>hangman` or `>hangman movies` -`>pick` | Picks the currency planted in this channel. 60 seconds cooldown. | `>pick` -`>plant` | Spend a unit of currency to plant it in this channel. (If bot is restarted or crashes, the currency will be lost) | `>plant` -`>gencurrency` `>gc` | Toggles currency generation on this channel. Every posted message will have chance to spawn currency. Chance is specified by the Bot Owner. (default is 2%) **Requires ManageMessages server permission.** | `>gc` -`>typestart` | Starts a typing contest. | `>typestart` -`>typestop` | Stops a typing contest on the current channel. | `>typestop` -`>typeadd` | Adds a new article to the typing contest. **Bot Owner only.** | `>typeadd wordswords` -`>typelist` | Lists added typing articles with their IDs. 15 per page. | `>typelist` or `>typelist 3` -`>typedel` | Deletes a typing article given the ID. **Bot Owner only.** | `>typedel 3` `>trivia` `>t` | Starts a game of trivia. You can add nohint to prevent hints.First player to get to 10 points wins by default. You can specify a different number. 30 seconds per question. | `>t` or `>t 5 nohint` -`>tl` | Shows a current trivia leaderboard. | `>tl` -`>tq` | Quits current trivia after current question. | `>tq` +`>tl` | Shows a current trivia leaderboard. | `>tl` +`>tq` | Quits current trivia after current question. | `>tq` +`>typestart` | Starts a typing contest. | `>typestart` +`>typestop` | Stops a typing contest on the current channel. | `>typestop` +`>typeadd` | Adds a new article to the typing contest. **Bot Owner only.** | `>typeadd wordswords` +`>typelist` | Lists added typing articles with their IDs. 15 per page. | `>typelist` or `>typelist 3` +`>typedel` | Deletes a typing article given the ID. **Bot Owner only.** | `>typedel 3` +`>poll` | Creates a poll which requires users to send the number of the voting option to the bot. **Requires ManageMessages server permission.** | `>poll Question?;Answer1;Answ 2;A_3` +`>publicpoll` `>ppoll` | Creates a public poll which requires users to type a number of the voting option in the channel command is ran in. **Requires ManageMessages server permission.** | `>ppoll Question?;Answer1;Answ 2;A_3` +`>pollstats` | Shows the poll results without stopping the poll on this server. **Requires ManageMessages server permission.** | `>pollstats` +`>pollend` | Stops active poll on this server and prints the results in this channel. **Requires ManageMessages server permission.** | `>pollend` +`>pick` | Picks the currency planted in this channel. 60 seconds cooldown. | `>pick` +`>plant` | Spend a unit of currency to plant it in this channel. (If bot is restarted or crashes, the currency will be lost) | `>plant` +`>gencurrency` `>gc` | Toggles currency generation on this channel. Every posted message will have chance to spawn currency. Chance is specified by the Bot Owner. (default is 2%) **Requires ManageMessages server permission.** | `>gc` +`>hangmanlist` | Shows a list of hangman term types. | `> hangmanlist` +`>hangman` | Starts a game of hangman in the channel. Use `>hangmanlist` to see a list of available term types. Defaults to 'all'. | `>hangman` or `>hangman movies` +`>cleverbot` | Toggles cleverbot session. When enabled, the bot will reply to messages starting with bot mention in the server. Custom reactions starting with %mention% won't work if cleverbot is enabled. **Requires ManageMessages server permission.** | `>cleverbot` +`>acrophobia` `>acro` | Starts an Acrophobia game. Second argment is optional round length in seconds. (default is 60) | `>acro` or `>acro 30` +`>choose` | Chooses a thing from a list of things | `>choose Get up;Sleep;Sleep more` +`>8ball` | Ask the 8ball a yes/no question. | `>8ball should I do something` +`>rps` | Play a game of rocket paperclip scissors with Nadeko. | `>rps scissors` +`>linux` | Prints a customizable Linux interjection | `>linux Spyware Windows` +`>leet` | Converts a text to leetspeak with 6 (1-6) severity levels | `>leet 3 Hello` ###### [Back to TOC](#table-of-contents) @@ -182,9 +186,9 @@ Command and aliases | Description | Usage `-modules` `-mdls` | Lists all bot modules. | `-modules` `-commands` `-cmds` | List all of the bot's commands from a certain module. You can either specify full, or only first few letters of the module name. | `-commands Administration` or `-cmds Admin` `-help` `-h` | Either shows a help for a single command, or DMs you help link if no arguments are specified. | `-h !!q` or `-h` -`-hgit` | Generates the commandlist.md file. **Bot Owner only.** | `-hgit` +`-hgit` | Generates the commandlist.md file. **Bot Owner only.** | `-hgit` `-readme` `-guide` | Sends a readme and a guide links to the channel. | `-readme` or `-guide` -`-donate` | Instructions for helping the project financially. | `-donate` +`-donate` | Instructions for helping the project financially. | `-donate` ###### [Back to TOC](#table-of-contents) @@ -215,11 +219,11 @@ Command and aliases | Description | Usage `!!setmaxplaytime` `!!smp` | Sets a maximum number of seconds (>14) a song can run before being skipped automatically. Set 0 to have no limit. | `!!smp 0` or `!!smp 270` `!!reptcursong` `!!rcs` | Toggles repeat of current song. | `!!rcs` `!!rpeatplaylst` `!!rpl` | Toggles repeat of all songs in the queue (every song that finishes is added to the end of the queue). | `!!rpl` -`!!save` | Saves a playlist under a certain name. Name must be no longer than 20 characters and mustn't contain dashes. | `!!save classical1` -`!!load` | Loads a saved playlist using it's ID. Use `!!pls` to list all saved playlists and !!save to save new ones. | `!!load 5` +`!!save` | Saves a playlist under a certain name. Name must be no longer than 20 characters and mustn't contain dashes. | `!!save classical1` +`!!load` | Loads a saved playlist using it's ID. Use `!!pls` to list all saved playlists and !!save to save new ones. | `!!load 5` `!!playlists` `!!pls` | Lists all playlists. Paginated. 20 per page. Default page is 0. | `!!pls 1` `!!deleteplaylist` `!!delpls` | Deletes a saved playlist. Only if you made it or if you are the bot owner. | `!!delpls animu-5` -`!!goto` | Goes to a specific time in seconds in a song. | `!!goto 30` +`!!goto` | Goes to a specific time in seconds in a song. | `!!goto 30` `!!autoplay` `!!ap` | Toggles autoplay - When the song is finished, automatically queue a related youtube song. (Works only for youtube songs and when queue is empty) | `!!ap` ###### [Back to TOC](#table-of-contents) @@ -227,17 +231,17 @@ Command and aliases | Description | Usage ### NSFW Command and aliases | Description | Usage ----------------|--------------|------- -`~hentai` | Shows a hentai image from a random website (gelbooru or danbooru or konachan or atfbooru or yandere) with a given tag. Tag is optional but preferred. Only 1 tag allowed. | `~hentai yuri` -`~autohentai` | Posts a hentai every X seconds with a random tag from the provided tags. Use `|` to separate tags. 20 seconds minimum. Provide no arguments to disable. | `~autohentai 30 yuri|tail|long_hair` or `~autohentai` -`~hentaibomb` | Shows a total 5 images (from gelbooru, danbooru, konachan, yandere and atfbooru). Tag is optional but preferred. | `~hentaibomb yuri` -`~danbooru` | Shows a random hentai image from danbooru with a given tag. Tag is optional but preferred. (multiple tags are appended with +) | `~danbooru yuri+kissing` -`~yandere` | Shows a random image from yandere with a given tag. Tag is optional but preferred. (multiple tags are appended with +) | `~yandere tag1+tag2` -`~konachan` | Shows a random hentai image from konachan with a given tag. Tag is optional but preferred. | `~konachan yuri` -`~gelbooru` | Shows a random hentai image from gelbooru with a given tag. Tag is optional but preferred. (multiple tags are appended with +) | `~gelbooru yuri+kissing` -`~rule34` | Shows a random image from rule34.xx with a given tag. Tag is optional but preferred. (multiple tags are appended with +) | `~rule34 yuri+kissing` -`~e621` | Shows a random hentai image from e621.net with a given tag. Tag is optional but preferred. Use spaces for multiple tags. | `~e621 yuri kissing` -`~cp` | We all know where this will lead you to. | `~cp` -`~boobs` | Real adult content. | `~boobs` +`~hentai` | Shows a hentai image from a random website (gelbooru or danbooru or konachan or atfbooru or yandere) with a given tag. Tag is optional but preferred. Only 1 tag allowed. | `~hentai yuri` +`~autohentai` | Posts a hentai every X seconds with a random tag from the provided tags. Use `|` to separate tags. 20 seconds minimum. Provide no arguments to disable. | `~autohentai 30 yuri|tail|long_hair` or `~autohentai` +`~hentaibomb` | Shows a total 5 images (from gelbooru, danbooru, konachan, yandere and atfbooru). Tag is optional but preferred. | `~hentaibomb yuri` +`~danbooru` | Shows a random hentai image from danbooru with a given tag. Tag is optional but preferred. (multiple tags are appended with +) | `~danbooru yuri+kissing` +`~yandere` | Shows a random image from yandere with a given tag. Tag is optional but preferred. (multiple tags are appended with +) | `~yandere tag1+tag2` +`~konachan` | Shows a random hentai image from konachan with a given tag. Tag is optional but preferred. | `~konachan yuri` +`~gelbooru` | Shows a random hentai image from gelbooru with a given tag. Tag is optional but preferred. (multiple tags are appended with +) | `~gelbooru yuri+kissing` +`~rule34` | Shows a random image from rule34.xx with a given tag. Tag is optional but preferred. (multiple tags are appended with +) | `~rule34 yuri+kissing` +`~e621` | Shows a random hentai image from e621.net with a given tag. Tag is optional but preferred. Use spaces for multiple tags. | `~e621 yuri kissing` +`~cp` | We all know where this will lead you to. | `~cp` +`~boobs` | Real adult content. | `~boobs` `~butts` `~ass` `~butt` | Real adult content. | `~butts` or `~ass` ###### [Back to TOC](#table-of-contents) @@ -245,6 +249,17 @@ Command and aliases | Description | Usage ### Permissions Command and aliases | Description | Usage ----------------|--------------|------- +`;srvrfilterinv` `;sfi` | Toggles automatic deleting of invites posted in the server. Does not affect Bot Owner. | `;sfi` +`;chnlfilterinv` `;cfi` | Toggles automatic deleting of invites posted in the channel. Does not negate the ;srvrfilterinv enabled setting. Does not affect Bot Owner. | `;cfi` +`;srvrfilterwords` `;sfw` | Toggles automatic deleting of messages containing forbidden words on the server. Does not affect Bot Owner. | `;sfw` +`;chnlfilterwords` `;cfw` | Toggles automatic deleting of messages containing banned words on the channel. Does not negate the ;srvrfilterwords enabled setting. Does not affect bot owner. | `;cfw` +`;fw` | Adds or removes (if it exists) a word from the list of filtered words. Use`;sfw` or `;cfw` to toggle filtering. | `;fw poop` +`;lstfilterwords` `;lfw` | Shows a list of filtered words. | `;lfw` +`;cmdcooldown` `;cmdcd` | Sets a cooldown per user for a command. Set to 0 to remove the cooldown. | `;cmdcd "some cmd" 5` +`;allcmdcooldowns` `;acmdcds` | Shows a list of all commands and their respective cooldowns. | `;acmdcds` +`;ubl` | Either [add]s or [rem]oves a user specified by a mention or ID from a blacklist. **Bot Owner only.** | `;ubl add @SomeUser` or `;ubl rem 12312312313` +`;cbl` | Either [add]s or [rem]oves a channel specified by an ID from a blacklist. **Bot Owner only.** | `;cbl rem 12312312312` +`;sbl` | Either [add]s or [rem]oves a server specified by a Name or ID from a blacklist. **Bot Owner only.** | `;sbl add 12312321312` or `;sbl rem SomeTrashServer` `;verbose` `;v` | Sets whether to show when a command/module is blocked. | `;verbose true` `;permrole` `;pr` | Sets a role which can change permissions. Or supply no parameters to find out the current one. Default one is 'Nadeko'. | `;pr role` `;listperms` `;lp` | Lists whole permission chain with their indexes. You can specify an optional page number if there are a lot of permissions. | `;lp` or `;lp 3` @@ -262,120 +277,111 @@ Command and aliases | Description | Usage `;allrolemdls` `;arm` | Enable or disable all modules for a specific role. | `;arm [enable/disable] MyRole` `;allusrmdls` `;aum` | Enable or disable all modules for a specific user. | `;aum enable @someone` `;allsrvrmdls` `;asm` | Enable or disable all modules for your server. | `;asm [enable/disable]` -`;ubl` | Either [add]s or [rem]oves a user specified by a mention or ID from a blacklist. **Bot Owner only.** | `;ubl add @SomeUser` or `;ubl rem 12312312313` -`;cbl` | Either [add]s or [rem]oves a channel specified by an ID from a blacklist. **Bot Owner only.** | `;cbl rem 12312312312` -`;sbl` | Either [add]s or [rem]oves a server specified by a Name or ID from a blacklist. **Bot Owner only.** | `;sbl add 12312321312` or `;sbl rem SomeTrashServer` -`;cmdcooldown` `;cmdcd` | Sets a cooldown per user for a command. Set to 0 to remove the cooldown. | `;cmdcd "some cmd" 5` -`;allcmdcooldowns` `;acmdcds` | Shows a list of all commands and their respective cooldowns. | `;acmdcds` -`;srvrfilterinv` `;sfi` | Toggles automatic deleting of invites posted in the server. Does not affect Bot Owner. | `;sfi` -`;chnlfilterinv` `;cfi` | Toggles automatic deleting of invites posted in the channel. Does not negate the ;srvrfilterinv enabled setting. Does not affect Bot Owner. | `;cfi` -`;srvrfilterwords` `;sfw` | Toggles automatic deleting of messages containing forbidden words on the server. Does not affect Bot Owner. | `;sfw` -`;chnlfilterwords` `;cfw` | Toggles automatic deleting of messages containing banned words on the channel. Does not negate the ;srvrfilterwords enabled setting. Does not affect bot owner. | `;cfw` -`;fw` | Adds or removes (if it exists) a word from the list of filtered words. Use`;sfw` or `;cfw` to toggle filtering. | `;fw poop` -`;lstfilterwords` `;lfw` | Shows a list of filtered words. | `;lfw` ###### [Back to TOC](#table-of-contents) ### Pokemon Command and aliases | Description | Usage ----------------|--------------|------- -`>attack` | Attacks a target with the given move. Use `>movelist` to see a list of moves your type can use. | `>attack "vine whip" @someguy` +`>attack` | Attacks a target with the given move. Use `>movelist` to see a list of moves your type can use. | `>attack "vine whip" @someguy` `>movelist` `>ml` | Lists the moves you are able to use | `>ml` -`>heal` | Heals someone. Revives those who fainted. Costs a NadekoFlower | `>heal @someone` -`>type` | Get the poketype of the target. | `>type @someone` -`>settype` | Set your poketype. Costs a NadekoFlower. Provide no arguments to see a list of available types. | `>settype fire` or `>settype` +`>heal` | Heals someone. Revives those who fainted. Costs a NadekoFlower | `>heal @someone` +`>type` | Get the poketype of the target. | `>type @someone` +`>settype` | Set your poketype. Costs a NadekoFlower. Provide no arguments to see a list of available types. | `>settype fire` or `>settype` ###### [Back to TOC](#table-of-contents) ### Searches Command and aliases | Description | Usage ----------------|--------------|------- -`~weather` `~we` | Shows weather data for a specified city. You can also specify a country after a comma. | `~we Moscow, RU` -`~youtube` `~yt` | Searches youtubes and shows the first result | `~yt query` -`~imdb` `~omdb` | Queries omdb for movies or series, show first result. | `~imdb Batman vs Superman` -`~randomcat` `~meow` | Shows a random cat image. | `~meow` -`~randomdog` `~woof` | Shows a random dog image. | `~woof` -`~img` `~i` | Pulls the first image found using a search parameter. Use ~ir for different results. | `~i cute kitten` -`~ir` | Pulls a random image using a search parameter. | `~ir cute kitten` -`~lmgtfy` | Google something for an idiot. | `~lmgtfy query` -`~shorten` | Attempts to shorten an URL, if it fails, returns the input URL. | `~shorten https://google.com` -`~google` `~g` | Get a google search link for some terms. | `~google query` -`~magicthegathering` `~mtg` | Searches for a Magic The Gathering card. | `~magicthegathering about face` or `~mtg about face` -`~hearthstone` `~hs` | Searches for a Hearthstone card and shows its image. Takes a while to complete. | `~hs Ysera` -`~yodify` `~yoda` | Translates your normal sentences into Yoda styled sentences! | ~yodify I was once an adventurer like you` or `~yoda my feelings hurt` -`~urbandict` `~ud` | Searches Urban Dictionary for a word. | `~ud Pineapple` -`~define` `~def` | Finds a definition of a word. | `~def heresy` -`~#` | Searches Tagdef.com for a hashtag. | `~# ff` -`~catfact` | Shows a random catfact from | `~catfact` -`~revav` | Returns a google reverse image search for someone's avatar. | `~revav "@SomeGuy"` -`~revimg` | Returns a google reverse image search for an image from a link. | `~revimg Image link` -`~safebooru` | Shows a random image from safebooru with a given tag. Tag is optional but preferred. (multiple tags are appended with +) | `~safebooru yuri+kissing` -`~wikipedia` `~wiki` | Gives you back a wikipedia link | `~wiki query` -`~color` `~clr` | Shows you what color corresponds to that hex. | `~clr 00ff00` -`~videocall` | Creates a private video call link for you and other mentioned people. The link is sent to mentioned people via a private message. | `~videocall "@SomeGuy"` -`~avatar` `~av` | Shows a mentioned person's avatar. | `~av "@SomeGuy"` -`~wikia` | Gives you back a wikia link | `~wikia mtg Vigilance` or `~wikia mlp Dashy` -`~minecraftping` `~mcping` | Pings a minecraft server. | `~mcping 127.0.0.1:25565` -`~minecraftquery` `~mcq` | Finds information about a minecraft server. | `~mcq server:ip` -`~lolban` | Shows top banned champions ordered by ban rate. | `~lolban` -`~memelist` | Pulls a list of memes you can use with `~memegen` from http://memegen.link/templates/ | `~memelist` -`~memegen` | Generates a meme from memelist with top and bottom text. | `~memegen biw "gets iced coffee" "in the winter"` -`~anime` `~ani` `~aq` | Queries anilist for an anime and shows the first result. | `~ani aquarion evol` -`~manga` `~mang` `~mq` | Queries anilist for a manga and shows the first result. | `~mq Shingeki no kyojin` -`~yomama` `~ym` | Shows a random joke from | `~ym` -`~randjoke` `~rj` | Shows a random joke from | `~rj` -`~chucknorris` `~cn` | Shows a random chucknorris joke from | `~cn` -`~wowjoke` | Get one of Kwoth's penultimate WoW jokes. | `~wowjoke` -`~magicitem` `~mi` | Shows a random magicitem from | `~mi` -`~osu` | Shows osu stats for a player. | `~osu Name` or `~osu Name taiko` -`~osub` | Shows information about an osu beatmap. | `~osub https://osu.ppy.sh/s/127712` -`~osu5` | Displays a user's top 5 plays. | `~osu5 Name` -`~overwatch` `~ow` | Show's basic stats on a player (competitive rank, playtime, level etc) Region codes are: `eu` `us` `cn` `kr` | `~ow us Battletag#1337` or `~overwatch eu Battletag#2016` -`~placelist` | Shows the list of available tags for the `~place` command. | `~placelist` -`~place` | Shows a placeholder image of a given tag. Use `~placelist` to see all available tags. You can specify the width and height of the image as the last two optional arguments. | `~place Cage` or `~place steven 500 400` -`~pokemon` `~poke` | Searches for a pokemon. | `~poke Sylveon` -`~pokemonability` `~pokeab` | Searches for a pokemon ability. | `~pokeab overgrow` +`~xkcd` | Shows a XKCD comic. No arguments will retrieve random one. Number argument will retrieve a specific comic, and "latest" will get the latest one. | `~xkcd` or `~xkcd 1400` or `~xkcd latest` +`~translate` `~trans` | Translates from>to text. From the given language to the destination language. | `~trans en>fr Hello` +`~autotrans` `~at` | Starts automatic translation of all messages by users who set their `~atl` in this channel. You can set "del" argument to automatically delete all translated user messages. **Requires Administrator server permission.** **Bot Owner only.** | `~at` or `~at del` +`~autotranslang` `~atl` | `~atl en>fr` | Sets your source and target language to be used with `~at`. Specify no arguments to remove previously set value. +`~translangs` | Lists the valid languages for translation. | `~translangs` `~hitbox` `~hb` | Notifies this channel when a certain user starts streaming. **Requires ManageMessages server permission.** | `~hitbox SomeStreamer` `~twitch` `~tw` | Notifies this channel when a certain user starts streaming. **Requires ManageMessages server permission.** | `~twitch SomeStreamer` `~beam` `~bm` | Notifies this channel when a certain user starts streaming. **Requires ManageMessages server permission.** | `~beam SomeStreamer` `~liststreams` `~ls` | Lists all streams you are following on this server. | `~ls` `~removestream` `~rms` | Removes notifications of a certain streamer from a certain platform on this channel. **Requires ManageMessages server permission.** | `~rms Twitch SomeGuy` or `~rms Beam SomeOtherGuy` `~checkstream` `~cs` | Checks if a user is online on a certain streaming platform. | `~cs twitch MyFavStreamer` -`~translate` `~trans` | Translates from>to text. From the given language to the destination language. | `~trans en>fr Hello` -`~autotrans` `~at` | Starts automatic translation of all messages by users who set their `~atl` in this channel. You can set "del" argument to automatically delete all translated user messages. **Requires Administrator server permission.** **Bot Owner only.** | `~at` or `~at del` -`~autotranslang` `~atl` | `~atl en>fr` | Sets your source and target language to be used with `~at`. Specify no arguments to remove previously set value. -`~translangs` | Lists the valid languages for translation. | `~translangs` -`~xkcd` | Shows a XKCD comic. No arguments will retrieve random one. Number argument will retrieve a specific comic, and "latest" will get the latest one. | `~xkcd` or `~xkcd 1400` or `~xkcd latest` +`~pokemon` `~poke` | Searches for a pokemon. | `~poke Sylveon` +`~pokemonability` `~pokeab` | Searches for a pokemon ability. | `~pokeab overgrow` +`~placelist` | Shows the list of available tags for the `~place` command. | `~placelist` +`~place` | Shows a placeholder image of a given tag. Use `~placelist` to see all available tags. You can specify the width and height of the image as the last two optional arguments. | `~place Cage` or `~place steven 500 400` +`~overwatch` `~ow` | Show's basic stats on a player (competitive rank, playtime, level etc) Region codes are: `eu` `us` `cn` `kr` | `~ow us Battletag#1337` or `~overwatch eu Battletag#2016` +`~osu` | Shows osu stats for a player. | `~osu Name` or `~osu Name taiko` +`~osub` | Shows information about an osu beatmap. | `~osub https://osu.ppy.sh/s/127712` +`~osu5` | Displays a user's top 5 plays. | `~osu5 Name` +`~yomama` `~ym` | Shows a random joke from | `~ym` +`~randjoke` `~rj` | Shows a random joke from | `~rj` +`~chucknorris` `~cn` | Shows a random chucknorris joke from | `~cn` +`~wowjoke` | Get one of Kwoth's penultimate WoW jokes. | `~wowjoke` +`~magicitem` `~mi` | Shows a random magicitem from | `~mi` +`~anime` `~ani` `~aq` | Queries anilist for an anime and shows the first result. | `~ani aquarion evol` +`~manga` `~mang` `~mq` | Queries anilist for a manga and shows the first result. | `~mq Shingeki no kyojin` +`~weather` `~we` | Shows weather data for a specified city. You can also specify a country after a comma. | `~we Moscow, RU` +`~youtube` `~yt` | Searches youtubes and shows the first result | `~yt query` +`~imdb` `~omdb` | Queries omdb for movies or series, show first result. | `~imdb Batman vs Superman` +`~randomcat` `~meow` | Shows a random cat image. | `~meow` +`~randomdog` `~woof` | Shows a random dog image. | `~woof` +`~image` `~img` | Pulls the first image found using a search parameter. Use ~rimg for different results. | `~img cute kitten` +`~randomimage` `~rimg` | Pulls a random image using a search parameter. | `~rimg cute kitten` +`~lmgtfy` | Google something for an idiot. | `~lmgtfy query` +`~shorten` | Attempts to shorten an URL, if it fails, returns the input URL. | `~shorten https://google.com` +`~google` `~g` | Get a google search link for some terms. | `~google query` +`~magicthegathering` `~mtg` | Searches for a Magic The Gathering card. | `~magicthegathering about face` or `~mtg about face` +`~hearthstone` `~hs` | Searches for a Hearthstone card and shows its image. Takes a while to complete. | `~hs Ysera` +`~yodify` `~yoda` | Translates your normal sentences into Yoda styled sentences! | ~yodify I was once an adventurer like you` or `~yoda my feelings hurt` +`~urbandict` `~ud` | Searches Urban Dictionary for a word. | `~ud Pineapple` +`~define` `~def` | Finds a definition of a word. | `~def heresy` +`~#` | Searches Tagdef.com for a hashtag. | `~# ff` +`~catfact` | Shows a random catfact from | `~catfact` +`~revav` | Returns a google reverse image search for someone's avatar. | `~revav "@SomeGuy"` +`~revimg` | Returns a google reverse image search for an image from a link. | `~revimg Image link` +`~safebooru` | Shows a random image from safebooru with a given tag. Tag is optional but preferred. (multiple tags are appended with +) | `~safebooru yuri+kissing` +`~wikipedia` `~wiki` | Gives you back a wikipedia link | `~wiki query` +`~color` `~clr` | Shows you what color corresponds to that hex. | `~clr 00ff00` +`~videocall` | Creates a private video call link for you and other mentioned people. The link is sent to mentioned people via a private message. | `~videocall "@SomeGuy"` +`~avatar` `~av` | Shows a mentioned person's avatar. | `~av "@SomeGuy"` +`~wikia` | Gives you back a wikia link | `~wikia mtg Vigilance` or `~wikia mlp Dashy` +`~minecraftping` `~mcping` | Pings a minecraft server. | `~mcping 127.0.0.1:25565` +`~minecraftquery` `~mcq` | Finds information about a minecraft server. | `~mcq server:ip` +`~lolban` | Shows top banned champions ordered by ban rate. | `~lolban` +`~memelist` | Pulls a list of memes you can use with `~memegen` from http://memegen.link/templates/ | `~memelist` +`~memegen` | Generates a meme from memelist with top and bottom text. | `~memegen biw "gets iced coffee" "in the winter"` ###### [Back to TOC](#table-of-contents) ### Utility Command and aliases | Description | Usage ----------------|--------------|------- -`.togethertube` `.totube` | Creates a new room on and shows the link in the chat. | `.totube` -`.whosplaying` `.whpl` | Shows a list of users who are playing the specified game. | `.whpl Overwatch` -`.inrole` | Lists every person from the provided role or roles (separated by a ',') on this server. If the list is too long for 1 message, you must have Manage Messages permission. | `.inrole Role` -`.checkmyperms` | Checks your user-specific permissions on this channel. | `.checkmyperms` -`.userid` `.uid` | Shows user ID. | `.uid` or `.uid "@SomeGuy"` -`.channelid` `.cid` | Shows current channel ID. | `.cid` -`.serverid` `.sid` | Shows current server ID. | `.sid` -`.roles` | List roles on this server or a roles of a specific user if specified. Paginated. 20 roles per page. | `.roles 2` or `.roles @Someone` -`.channeltopic` `.ct` | Sends current channel's topic as a message. | `.ct` -`.stats` | Shows some basic stats for Nadeko. | `.stats` -`.showemojis` `.se` | Shows a name and a link to every SPECIAL emoji in the message. | `.se A message full of SPECIAL emojis` -`.listservers` | Lists servers the bot is on with some basic info. 15 per page. **Bot Owner only.** | `.listservers 3` -`.calculate` `.calc` | Evaluate a mathematical expression. | `.calc 1+1` -`.calcops` | Shows all available operations in .calc command | `.calcops` +`.convertlist` | List of the convertible dimensions and currencies. | `.convertlist` +`.convert` | Convert quantities. Use `.convertlist` to see supported dimensions and currencies. | `.convert m km 1000` +`.remind` | Sends a message to you or a channel after certain amount of time. First argument is me/here/'channelname'. Second argument is time in a descending order (mo>w>d>h>m) example: 1w5d3h10m. Third argument is a (multiword)message. | `.remind me 1d5h Do something` or `.remind #general 1m Start now!` +`.remindtemplate` | Sets message for when the remind is triggered. Available placeholders are %user% - user who ran the command, %message% - Message specified in the remind, %target% - target channel of the remind. **Bot Owner only.** | `.remindtemplate %user%, do %message%!` +`.listquotes` `.liqu` | `.liqu` or `.liqu 3` | Lists all quotes on the server ordered alphabetically. 15 Per page. +`...` | Shows a random quote with a specified name. | `... abc` +`..` | Adds a new quote with the specified name and message. | `.. sayhi Hi` +`.deletequote` `.delq` | Deletes a random quote with the specified keyword. You have to either be server Administrator or the creator of the quote to delete it. | `.delq abc` +`.delallq` `.daq` | Deletes all quotes on a specified keyword. **Requires Administrator server permission.** | `.delallq kek` `.serverinfo` `.sinfo` | Shows info about the server the bot is on. If no channel is supplied, it defaults to current one. | `.sinfo Some Server` `.channelinfo` `.cinfo` | Shows info about the channel. If no channel is supplied, it defaults to current one. | `.cinfo #some-channel` `.userinfo` `.uinfo` | Shows info about the user. If no user is supplied, it defaults a user running the command. | `.uinfo @SomeUser` -`.activity` | Checks for spammers. **Bot Owner only.** | `.activity` -`.listquotes` `.liqu` | `.liqu` or `.liqu 3` | Lists all quotes on the server ordered alphabetically. 15 Per page. -`...` | Shows a random quote with a specified name. | `... abc` -`..` | Adds a new quote with the specified name and message. | `.. sayhi Hi` -`.deletequote` `.delq` | Deletes a random quote with the specified keyword. You have to either be server Administrator or the creator of the quote to delete it. | `.delq abc` -`.delallq` `.daq` | Deletes all quotes on a specified keyword. **Requires Administrator server permission.** | `.delallq kek` -`.remind` | Sends a message to you or a channel after certain amount of time. First argument is me/here/'channelname'. Second argument is time in a descending order (mo>w>d>h>m) example: 1w5d3h10m. Third argument is a (multiword)message. | `.remind me 1d5h Do something` or `.remind #general 1m Start now!` -`.remindtemplate` | Sets message for when the remind is triggered. Available placeholders are %user% - user who ran the command, %message% - Message specified in the remind, %target% - target channel of the remind. **Bot Owner only.** | `.remindtemplate %user%, do %message%!` -`.convertlist` | List of the convertible dimensions and currencies. | `.convertlist` -`.convert` | Convert quantities. Use `.convertlist` to see supported dimensions and currencies. | `.convert m km 1000` +`.calculate` `.calc` | Evaluate a mathematical expression. | `.calc 1+1` +`.calcops` | Shows all available operations in .calc command | `.calcops` +`.rotaterolecolor` `.rrc` | Rotates a roles color on an interval with a list of supplied colors. First argument is interval in seconds (Minimum 60). Second argument is a role, followed by a space-separated list of colors in hex. Provide a rolename with a 0 interval to disable. **Bot Owner only.** | `.rrc 60 MyLsdRole #ff0000 #00ff00 #0000ff` or `.rrc 0 MyLsdRole` +`.togethertube` `.totube` | Creates a new room on and shows the link in the chat. | `.totube` +`.whosplaying` `.whpl` | Shows a list of users who are playing the specified game. | `.whpl Overwatch` +`.inrole` | Lists every person from the provided role or roles (separated by a ',') on this server. If the list is too long for 1 message, you must have Manage Messages permission. | `.inrole Role` +`.checkmyperms` | Checks your user-specific permissions on this channel. | `.checkmyperms` +`.userid` `.uid` | Shows user ID. | `.uid` or `.uid "@SomeGuy"` +`.channelid` `.cid` | Shows current channel ID. | `.cid` +`.serverid` `.sid` | Shows current server ID. | `.sid` +`.roles` | List roles on this server or a roles of a specific user if specified. Paginated. 20 roles per page. | `.roles 2` or `.roles @Someone` +`.channeltopic` `.ct` | Sends current channel's topic as a message. | `.ct` +`.createinvite` `.crinv` | Creates a new invite which has infinite max uses and never expires. **Requires CreateInstantInvite channel permission.** | `.crinv` +`.stats` | Shows some basic stats for Nadeko. | `.stats` +`.showemojis` `.se` | Shows a name and a link to every SPECIAL emoji in the message. | `.se A message full of SPECIAL emojis` +`.listservers` | Lists servers the bot is on with some basic info. 15 per page. **Bot Owner only.** | `.listservers 3` +`.activity` | Checks for spammers. **Bot Owner only.** | `.activity` diff --git a/docs/Custom Reactions.md b/docs/Custom Reactions.md index 3d99cb23..8d41afca 100644 --- a/docs/Custom Reactions.md +++ b/docs/Custom Reactions.md @@ -40,7 +40,7 @@ There are currently three different placeholders which we will look at, with mor | Placeholder | Description | Example Usage | Usage | |:-----------:|-------------|---------------|-------| -|`%mention`|The `%mention%` placeholder is triggered when you type `@BotName` - It's important to note that if you've given the bot a custom nickname, this trigger won't work!|```.acr "Hello %mention%" I, %mention%, also say hello!```|Input: "Hello @BotName" Output: "I, @BotName, also say hello!"| +|`%mention%`|The `%mention%` placeholder is triggered when you type `@BotName` - It's important to note that if you've given the bot a custom nickname, this trigger won't work!|```.acr "Hello %mention%" I, %mention%, also say hello!```|Input: "Hello @BotName" Output: "I, @BotName, also say hello!"| |`%user%`|The `%user%` placeholder mentions the person who said the command|`.acr "Who am I?" You are %user%!`|Input: "Who am I?" Output: "You are @Username!"| |`%rng%`|The `%rng%` placeholder generates a random number between 0 and 10. You can also specify a custom range (%rng1-100%) even with negative numbers: `%rng-9--1%` (from -9 to -1) . |`.acr "Random number" %rng%`|Input: "Random number" Output: "2"| |`%rnduser%`|The `%rnduser%` placeholder mentions a random user from the server. |`.acr "Random user" %rnduser%`|Input: "Random number" Output: @SomeUser| diff --git a/docs/Permissions System.md b/docs/Permissions System.md index 11c32daf..99f97e04 100644 --- a/docs/Permissions System.md +++ b/docs/Permissions System.md @@ -43,7 +43,7 @@ Commonly Asked Questions --------------- ###How do I create a music DJ? -To allow users to only see the current song and have a DJ role for queuing follow these five steps: +To allow users to only see the current song and have a DJ role for queuing follow these steps: 1. `;sm Music disable` @@ -53,17 +53,13 @@ To allow users to only see the current song and have a DJ role for queuing follo * Enables the "nowplaying" command for everyone -3. `;sc !!getlink enable` - - * Enables the "getlink" command for everyone - -4. `;sc !!listqueue enable` +3. `;sc !!listqueue enable` * Enables the "listqueue" command for everyone -5. `;rm Music enable DJ` +4. `;rm Music enable DJ` - * Enables all the music commands only for the DJ role + * Enables all music commands only for the DJ role ###How do I create a NSFW channel? diff --git a/docs/guides/Docker Guide.md b/docs/guides/Docker Guide.md index 9908a477..474337d6 100644 --- a/docs/guides/Docker Guide.md +++ b/docs/guides/Docker Guide.md @@ -1,34 +1,58 @@ # NadekoBot a Discord bot -Nadeko is written in C# and Discord.net for more information visit https://github.com/Kwoth/NadekoBot +Nadeko is written in C# and Discord.net for more information visit ## Install Docker -Follow the respective guide for your operating system found here https://docs.docker.com/engine/installation/ +Follow the respective guide for your operating system found here [Docker Engine Install Guide](https://docs.docker.com/engine/installation/) ## Nadeko Setup Guide For this guide we will be using the folder /nadeko as our config root folder. -``` -docker create --name=nadeko -v /nadeko/data:/opt/NadekoBot/src/NadekoBot/bin/Release/netcoreapp1.0/data -v /nadeko/credentials.json:/opt/NadekoBot/src/NadekoBot/credentials.json kwoth/nadeko:dev +```bash +docker create --name=nadeko -v /nadeko/data:/opt/NadekoBot/src/NadekoBot/bin/Release/netcoreapp1.0/data -v /nadeko/credentials.json:/opt/NadekoBot/src/NadekoBot/credentials.json uirel/nadeko ``` -If you are coming from a previous version of nadeko (the old docker) make sure your crednetials.json has been copied into this directory and is the only thing in this folder. --If you are making a fresh install, create your credentials.json from the following guide and palce it in the /nadeko folder -http://nadekobot.readthedocs.io/en/latest/JSON%20Explanations/ +-If you are making a fresh install, create your credentials.json from the following guide and palce it in the /nadeko folder [Nadeko JSON Guide](http://nadekobot.readthedocs.io/en/latest/JSON%20Explanations/) Next start the docker up with -```docker start nadeko; docker logs -f nadeko``` +`docker start nadeko; docker logs -f nadeko` The docker will start and the log file will start scrolling past. Depending on hardware the bot start can take up to 5 minutes on a small DigitalOcean droplet. Once the log ends with "NadekoBot | Starting NadekoBot v1.0-rc2" the bot is ready and can be invited to your server. Ctrl+C at this point to stop viewing the logs. After a few moments you should be able to invite Nadeko to your server. If you cannot check the log file for errors -## Updates / Monitoring +## Monitoring -* Upgrade to the latest version of Nadeko simply `docker restart nadeko`. * Monitor the logs of the container in realtime `docker logs -f nadeko`. +## Updates + +# Manual +Updates are handled by pulling the new layer of the Docker Container which contains a pre compiled update to Nadeko. +The following commands are required for the default options + +`docker pull uirel/nadeko:latest` + +`docker stop nadeko; docker rm nadeko` + +`docker create --name=nadeko -v /nadeko/data:/opt/NadekoBot/src/NadekoBot/bin/Release/netcoreapp1.0/data -v /nadeko/credentials.json:/opt/NadekoBot/src/NadekoBot/credentials.json uirel/nadeko` + +`docker start nadeko` + + +# Automatic Updates +Automatic update are now handled by watchertower [WatchTower GitHub](https://github.com/CenturyLinkLabs/watchtower) +To setup watchtower to keep Nadeko up-to-date for you with the default settings use the following command + +```bash +docker run -d --name watchtower -v /var/run/docker.sock:/var/run/docker.sock centurylink/watchtower --cleanup nadeko +``` + +This will check for updates to the docker every 5 minutes and update immediately. Alternatively using the `--interval X` command to change the interval, where X is the amount of time in seconds to wait. eg 21600 for 6 hours. + + If you have any issues with the docker setup, please ask in #help but indicate you are using the docker. -For information about configuring your bot or its functionality, please check the http://nadekobot.readthedocs.io/en/latest guides. +For information about configuring your bot or its functionality, please check the guides. diff --git a/docs/index.md b/docs/index.md index 872e2e40..c2dbfa0a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -11,6 +11,9 @@ NadekoBot is an open source project, and it can be found on our [GitHub][GitHub] Here you can read current [Issues][Issues]. If you want to contribute, be sure to PR on the **[dev][dev]** branch. + +**NadekoBot 1.1 release currently does not support x86/32bit architecture.** + ##Content - [About](about.md) - Guides @@ -34,14 +37,3 @@ If you want to contribute, be sure to PR on the **[dev][dev]** branch. [GitHub]: https://github.com/Kwoth/NadekoBot [Issues]: https://github.com/Kwoth/NadekoBot/issues [dev]: https://github.com/Kwoth/NadekoBot/tree/dev - -[Italian]: http://i.imgur.com/SsaTwOF.png?1 -[Russian]: http://i.imgur.com/wf9bc5G.png?1 -[German]: http://i.imgur.com/EM5qPzf.png?1 -[Chinese]: http://i.imgur.com/MVCNOjT.png?1 -[English]: http://i.imgur.com/jHTyZFS.png?1 -[Spanish]: http://i.imgur.com/9BsusB6.png?1 -[French]: http://i.imgur.com/g2ARPF6.png?1 -[Dutch]: http://i.imgur.com/SadddLj.png?1 -[Norwegian]: http://i.imgur.com/TCVa0V8.png?1 -[Serbian]: http://i.imgur.com/5evoUbU.png diff --git a/src/NadekoBot/DataStructures/ExecuteCommandResult.cs b/src/NadekoBot/DataStructures/ExecuteCommandResult.cs new file mode 100644 index 00000000..c8568c43 --- /dev/null +++ b/src/NadekoBot/DataStructures/ExecuteCommandResult.cs @@ -0,0 +1,24 @@ +using Discord.Commands; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using static NadekoBot.Modules.Permissions.Permissions; + +namespace NadekoBot.DataStructures +{ + public struct ExecuteCommandResult + { + public readonly CommandInfo CommandInfo; + public readonly PermissionCache PermissionCache; + public readonly IResult Result; + + public ExecuteCommandResult(CommandInfo commandInfo, PermissionCache cache, IResult result) + { + this.CommandInfo = commandInfo; + this.PermissionCache = cache; + this.Result = result; + } + } +} diff --git a/src/NadekoBot/Migrations/20170110111159_repeater-drop.Designer.cs b/src/NadekoBot/Migrations/20170110111159_repeater-drop.Designer.cs new file mode 100644 index 00000000..db95f728 --- /dev/null +++ b/src/NadekoBot/Migrations/20170110111159_repeater-drop.Designer.cs @@ -0,0 +1,834 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using NadekoBot.Services.Database; +using NadekoBot.Services.Database.Models; +using NadekoBot.Modules.Music.Classes; + +namespace NadekoBot.Migrations +{ + [DbContext(typeof(NadekoContext))] + [Migration("20170110111159_repeater-drop")] + partial class repeaterdrop + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.1.0-rtm-22752"); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlacklistItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("ItemId"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("BlacklistItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BotConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BufferSize"); + + b.Property("CurrencyGenerationChance"); + + b.Property("CurrencyGenerationCooldown"); + + b.Property("CurrencyName"); + + b.Property("CurrencyPluralName"); + + b.Property("CurrencySign"); + + b.Property("DMHelpString"); + + b.Property("ForwardMessages"); + + b.Property("ForwardToAllOwners"); + + b.Property("HelpString"); + + b.Property("MigrationVersion"); + + b.Property("RemindMessageFormat"); + + b.Property("RotatingStatuses"); + + b.HasKey("Id"); + + b.ToTable("BotConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashCaller", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BaseDestroyed"); + + b.Property("CallUser"); + + b.Property("ClashWarId"); + + b.Property("SequenceNumber"); + + b.Property("Stars"); + + b.Property("TimeAdded"); + + b.HasKey("Id"); + + b.HasIndex("ClashWarId"); + + b.ToTable("ClashCallers"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashWar", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("EnemyClan"); + + b.Property("GuildId"); + + b.Property("Size"); + + b.Property("StartedAt"); + + b.Property("WarState"); + + b.HasKey("Id"); + + b.ToTable("ClashOfClans"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CommandName"); + + b.Property("GuildConfigId"); + + b.Property("Seconds"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("CommandCooldown"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ConvertUnit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("InternalTrigger"); + + b.Property("Modifier"); + + b.Property("UnitType"); + + b.HasKey("Id"); + + b.ToTable("ConversionUnits"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CurrencyTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("Reason"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.ToTable("CurrencyTransactions"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CustomReaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("GuildId"); + + b.Property("IsRegex"); + + b.Property("OwnerOnly"); + + b.Property("Response"); + + b.Property("Trigger"); + + b.HasKey("Id"); + + b.ToTable("CustomReactions"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Donator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("Name"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Donators"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.EightBallResponse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("Text"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("EightBallResponses"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("GuildConfigId"); + + b.Property("GuildConfigId1"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.HasIndex("GuildConfigId1"); + + b.ToTable("FilterChannelId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("GuildConfigId"); + + b.Property("Word"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FilteredWord"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FollowedStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("GuildConfigId"); + + b.Property("GuildId"); + + b.Property("Type"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FollowedStream"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("GuildConfigId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("GCChannelId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AutoAssignRoleId"); + + b.Property("AutoDeleteByeMessages"); + + b.Property("AutoDeleteByeMessagesTimer"); + + b.Property("AutoDeleteGreetMessages"); + + b.Property("AutoDeleteGreetMessagesTimer"); + + b.Property("AutoDeleteSelfAssignedRoleMessages"); + + b.Property("ByeMessageChannelId"); + + b.Property("ChannelByeMessageText"); + + b.Property("ChannelGreetMessageText"); + + b.Property("CleverbotEnabled"); + + b.Property("DefaultMusicVolume"); + + b.Property("DeleteMessageOnCommand"); + + b.Property("DmGreetMessageText"); + + b.Property("ExclusiveSelfAssignedRoles"); + + b.Property("FilterInvites"); + + b.Property("FilterWords"); + + b.Property("GreetMessageChannelId"); + + b.Property("GuildId"); + + b.Property("LogSettingId"); + + b.Property("MuteRoleName"); + + b.Property("PermissionRole"); + + b.Property("RootPermissionId"); + + b.Property("SendChannelByeMessage"); + + b.Property("SendChannelGreetMessage"); + + b.Property("SendDmGreetMessage"); + + b.Property("VerbosePermissions"); + + b.Property("VoicePlusTextEnabled"); + + b.HasKey("Id"); + + b.HasIndex("GuildId") + .IsUnique(); + + b.HasIndex("LogSettingId"); + + b.HasIndex("RootPermissionId"); + + b.ToTable("GuildConfigs"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("LogSettingId"); + + b.HasKey("Id"); + + b.HasIndex("LogSettingId"); + + b.ToTable("IgnoredLogChannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("LogSettingId"); + + b.HasKey("Id"); + + b.HasIndex("LogSettingId"); + + b.ToTable("IgnoredVoicePresenceCHannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.LogSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelCreated"); + + b.Property("ChannelCreatedId"); + + b.Property("ChannelDestroyed"); + + b.Property("ChannelDestroyedId"); + + b.Property("ChannelId"); + + b.Property("ChannelUpdated"); + + b.Property("ChannelUpdatedId"); + + b.Property("IsLogging"); + + b.Property("LogOtherId"); + + b.Property("LogUserPresence"); + + b.Property("LogUserPresenceId"); + + b.Property("LogVoicePresence"); + + b.Property("LogVoicePresenceId"); + + b.Property("LogVoicePresenceTTSId"); + + b.Property("MessageDeleted"); + + b.Property("MessageDeletedId"); + + b.Property("MessageUpdated"); + + b.Property("MessageUpdatedId"); + + b.Property("UserBanned"); + + b.Property("UserBannedId"); + + b.Property("UserJoined"); + + b.Property("UserJoinedId"); + + b.Property("UserLeft"); + + b.Property("UserLeftId"); + + b.Property("UserMutedId"); + + b.Property("UserPresenceChannelId"); + + b.Property("UserUnbanned"); + + b.Property("UserUnbannedId"); + + b.Property("UserUpdated"); + + b.Property("UserUpdatedId"); + + b.Property("VoicePresenceChannelId"); + + b.HasKey("Id"); + + b.ToTable("LogSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ModulePrefix", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("ModuleName"); + + b.Property("Prefix"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("ModulePrefixes"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MusicPlaylist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Author"); + + b.Property("AuthorId"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.ToTable("MusicPlaylists"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("GuildConfigId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("MutedUserId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("NextId"); + + b.Property("PrimaryTarget"); + + b.Property("PrimaryTargetId"); + + b.Property("SecondaryTarget"); + + b.Property("SecondaryTargetName"); + + b.Property("State"); + + b.HasKey("Id"); + + b.HasIndex("NextId") + .IsUnique(); + + b.ToTable("Permission"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlayingStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("Status"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("PlayingStatus"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("MusicPlaylistId"); + + b.Property("Provider"); + + b.Property("ProviderType"); + + b.Property("Query"); + + b.Property("Title"); + + b.Property("Uri"); + + b.HasKey("Id"); + + b.HasIndex("MusicPlaylistId"); + + b.ToTable("PlaylistSong"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Quote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuthorId"); + + b.Property("AuthorName") + .IsRequired(); + + b.Property("GuildId"); + + b.Property("Keyword") + .IsRequired(); + + b.Property("Text") + .IsRequired(); + + b.HasKey("Id"); + + b.ToTable("Quotes"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RaceAnimal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("Icon"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("RaceAnimals"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Reminder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("IsPrivate"); + + b.Property("Message"); + + b.Property("ServerId"); + + b.Property("UserId"); + + b.Property("When"); + + b.HasKey("Id"); + + b.ToTable("Reminders"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SelfAssignedRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("GuildId"); + + b.Property("RoleId"); + + b.HasKey("Id"); + + b.HasIndex("GuildId", "RoleId") + .IsUnique(); + + b.ToTable("SelfAssignableRoles"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UserPokeTypes", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("UserId"); + + b.Property("type"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("PokeGame"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlacklistItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("Blacklist") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashCaller", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ClashWar", "ClashWar") + .WithMany("Bases") + .HasForeignKey("ClashWarId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("CommandCooldowns") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.EightBallResponse", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("EightBallResponses") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilterInvitesChannelIds") + .HasForeignKey("GuildConfigId"); + + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilterWordsChannelIds") + .HasForeignKey("GuildConfigId1"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilteredWords") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FollowedStream", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FollowedStreams") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("GenerateCurrencyChannelIds") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany() + .HasForeignKey("LogSettingId"); + + b.HasOne("NadekoBot.Services.Database.Models.Permission", "RootPermission") + .WithMany() + .HasForeignKey("RootPermissionId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany("IgnoredChannels") + .HasForeignKey("LogSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany("IgnoredVoicePresenceChannelIds") + .HasForeignKey("LogSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ModulePrefix", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("ModulePrefixes") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("MutedUsers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permission", b => + { + b.HasOne("NadekoBot.Services.Database.Models.Permission", "Next") + .WithOne("Previous") + .HasForeignKey("NadekoBot.Services.Database.Models.Permission", "NextId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlayingStatus", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("RotatingStatusMessages") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.HasOne("NadekoBot.Services.Database.Models.MusicPlaylist") + .WithMany("Songs") + .HasForeignKey("MusicPlaylistId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RaceAnimal", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("RaceAnimals") + .HasForeignKey("BotConfigId"); + }); + } + } +} diff --git a/src/NadekoBot/Migrations/20170110111159_repeater-drop.cs b/src/NadekoBot/Migrations/20170110111159_repeater-drop.cs new file mode 100644 index 00000000..d05e6672 --- /dev/null +++ b/src/NadekoBot/Migrations/20170110111159_repeater-drop.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace NadekoBot.Migrations +{ + public partial class repeaterdrop : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Repeaters"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Repeaters", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ChannelId = table.Column(nullable: false), + GuildId = table.Column(nullable: false), + Interval = table.Column(nullable: false), + Message = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Repeaters", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_Repeaters_ChannelId", + table: "Repeaters", + column: "ChannelId", + unique: true); + } + } +} diff --git a/src/NadekoBot/Migrations/20170110111302_repeater-new.Designer.cs b/src/NadekoBot/Migrations/20170110111302_repeater-new.Designer.cs new file mode 100644 index 00000000..09bee367 --- /dev/null +++ b/src/NadekoBot/Migrations/20170110111302_repeater-new.Designer.cs @@ -0,0 +1,863 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using NadekoBot.Services.Database; +using NadekoBot.Services.Database.Models; +using NadekoBot.Modules.Music.Classes; + +namespace NadekoBot.Migrations +{ + [DbContext(typeof(NadekoContext))] + [Migration("20170110111302_repeater-new")] + partial class repeaternew + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.1.0-rtm-22752"); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlacklistItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("ItemId"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("BlacklistItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BotConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BufferSize"); + + b.Property("CurrencyGenerationChance"); + + b.Property("CurrencyGenerationCooldown"); + + b.Property("CurrencyName"); + + b.Property("CurrencyPluralName"); + + b.Property("CurrencySign"); + + b.Property("DMHelpString"); + + b.Property("ForwardMessages"); + + b.Property("ForwardToAllOwners"); + + b.Property("HelpString"); + + b.Property("MigrationVersion"); + + b.Property("RemindMessageFormat"); + + b.Property("RotatingStatuses"); + + b.HasKey("Id"); + + b.ToTable("BotConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashCaller", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BaseDestroyed"); + + b.Property("CallUser"); + + b.Property("ClashWarId"); + + b.Property("SequenceNumber"); + + b.Property("Stars"); + + b.Property("TimeAdded"); + + b.HasKey("Id"); + + b.HasIndex("ClashWarId"); + + b.ToTable("ClashCallers"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashWar", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("EnemyClan"); + + b.Property("GuildId"); + + b.Property("Size"); + + b.Property("StartedAt"); + + b.Property("WarState"); + + b.HasKey("Id"); + + b.ToTable("ClashOfClans"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CommandName"); + + b.Property("GuildConfigId"); + + b.Property("Seconds"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("CommandCooldown"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ConvertUnit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("InternalTrigger"); + + b.Property("Modifier"); + + b.Property("UnitType"); + + b.HasKey("Id"); + + b.ToTable("ConversionUnits"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CurrencyTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("Reason"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.ToTable("CurrencyTransactions"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CustomReaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("GuildId"); + + b.Property("IsRegex"); + + b.Property("OwnerOnly"); + + b.Property("Response"); + + b.Property("Trigger"); + + b.HasKey("Id"); + + b.ToTable("CustomReactions"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Donator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("Name"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Donators"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.EightBallResponse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("Text"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("EightBallResponses"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("GuildConfigId"); + + b.Property("GuildConfigId1"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.HasIndex("GuildConfigId1"); + + b.ToTable("FilterChannelId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("GuildConfigId"); + + b.Property("Word"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FilteredWord"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FollowedStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("GuildConfigId"); + + b.Property("GuildId"); + + b.Property("Type"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FollowedStream"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("GuildConfigId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("GCChannelId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AutoAssignRoleId"); + + b.Property("AutoDeleteByeMessages"); + + b.Property("AutoDeleteByeMessagesTimer"); + + b.Property("AutoDeleteGreetMessages"); + + b.Property("AutoDeleteGreetMessagesTimer"); + + b.Property("AutoDeleteSelfAssignedRoleMessages"); + + b.Property("ByeMessageChannelId"); + + b.Property("ChannelByeMessageText"); + + b.Property("ChannelGreetMessageText"); + + b.Property("CleverbotEnabled"); + + b.Property("DefaultMusicVolume"); + + b.Property("DeleteMessageOnCommand"); + + b.Property("DmGreetMessageText"); + + b.Property("ExclusiveSelfAssignedRoles"); + + b.Property("FilterInvites"); + + b.Property("FilterWords"); + + b.Property("GreetMessageChannelId"); + + b.Property("GuildId"); + + b.Property("LogSettingId"); + + b.Property("MuteRoleName"); + + b.Property("PermissionRole"); + + b.Property("RootPermissionId"); + + b.Property("SendChannelByeMessage"); + + b.Property("SendChannelGreetMessage"); + + b.Property("SendDmGreetMessage"); + + b.Property("VerbosePermissions"); + + b.Property("VoicePlusTextEnabled"); + + b.HasKey("Id"); + + b.HasIndex("GuildId") + .IsUnique(); + + b.HasIndex("LogSettingId"); + + b.HasIndex("RootPermissionId"); + + b.ToTable("GuildConfigs"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildRepeater", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("GuildConfigId"); + + b.Property("GuildId"); + + b.Property("Interval"); + + b.Property("Message"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("GuildRepeater"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("LogSettingId"); + + b.HasKey("Id"); + + b.HasIndex("LogSettingId"); + + b.ToTable("IgnoredLogChannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("LogSettingId"); + + b.HasKey("Id"); + + b.HasIndex("LogSettingId"); + + b.ToTable("IgnoredVoicePresenceCHannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.LogSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelCreated"); + + b.Property("ChannelCreatedId"); + + b.Property("ChannelDestroyed"); + + b.Property("ChannelDestroyedId"); + + b.Property("ChannelId"); + + b.Property("ChannelUpdated"); + + b.Property("ChannelUpdatedId"); + + b.Property("IsLogging"); + + b.Property("LogOtherId"); + + b.Property("LogUserPresence"); + + b.Property("LogUserPresenceId"); + + b.Property("LogVoicePresence"); + + b.Property("LogVoicePresenceId"); + + b.Property("LogVoicePresenceTTSId"); + + b.Property("MessageDeleted"); + + b.Property("MessageDeletedId"); + + b.Property("MessageUpdated"); + + b.Property("MessageUpdatedId"); + + b.Property("UserBanned"); + + b.Property("UserBannedId"); + + b.Property("UserJoined"); + + b.Property("UserJoinedId"); + + b.Property("UserLeft"); + + b.Property("UserLeftId"); + + b.Property("UserMutedId"); + + b.Property("UserPresenceChannelId"); + + b.Property("UserUnbanned"); + + b.Property("UserUnbannedId"); + + b.Property("UserUpdated"); + + b.Property("UserUpdatedId"); + + b.Property("VoicePresenceChannelId"); + + b.HasKey("Id"); + + b.ToTable("LogSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ModulePrefix", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("ModuleName"); + + b.Property("Prefix"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("ModulePrefixes"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MusicPlaylist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Author"); + + b.Property("AuthorId"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.ToTable("MusicPlaylists"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("GuildConfigId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("MutedUserId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("NextId"); + + b.Property("PrimaryTarget"); + + b.Property("PrimaryTargetId"); + + b.Property("SecondaryTarget"); + + b.Property("SecondaryTargetName"); + + b.Property("State"); + + b.HasKey("Id"); + + b.HasIndex("NextId") + .IsUnique(); + + b.ToTable("Permission"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlayingStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("Status"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("PlayingStatus"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("MusicPlaylistId"); + + b.Property("Provider"); + + b.Property("ProviderType"); + + b.Property("Query"); + + b.Property("Title"); + + b.Property("Uri"); + + b.HasKey("Id"); + + b.HasIndex("MusicPlaylistId"); + + b.ToTable("PlaylistSong"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Quote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuthorId"); + + b.Property("AuthorName") + .IsRequired(); + + b.Property("GuildId"); + + b.Property("Keyword") + .IsRequired(); + + b.Property("Text") + .IsRequired(); + + b.HasKey("Id"); + + b.ToTable("Quotes"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RaceAnimal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("Icon"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("RaceAnimals"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Reminder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("IsPrivate"); + + b.Property("Message"); + + b.Property("ServerId"); + + b.Property("UserId"); + + b.Property("When"); + + b.HasKey("Id"); + + b.ToTable("Reminders"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SelfAssignedRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("GuildId"); + + b.Property("RoleId"); + + b.HasKey("Id"); + + b.HasIndex("GuildId", "RoleId") + .IsUnique(); + + b.ToTable("SelfAssignableRoles"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UserPokeTypes", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("UserId"); + + b.Property("type"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("PokeGame"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlacklistItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("Blacklist") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashCaller", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ClashWar", "ClashWar") + .WithMany("Bases") + .HasForeignKey("ClashWarId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("CommandCooldowns") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.EightBallResponse", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("EightBallResponses") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilterInvitesChannelIds") + .HasForeignKey("GuildConfigId"); + + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilterWordsChannelIds") + .HasForeignKey("GuildConfigId1"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilteredWords") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FollowedStream", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FollowedStreams") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("GenerateCurrencyChannelIds") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany() + .HasForeignKey("LogSettingId"); + + b.HasOne("NadekoBot.Services.Database.Models.Permission", "RootPermission") + .WithMany() + .HasForeignKey("RootPermissionId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildRepeater", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("GuildRepeaters") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany("IgnoredChannels") + .HasForeignKey("LogSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany("IgnoredVoicePresenceChannelIds") + .HasForeignKey("LogSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ModulePrefix", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("ModulePrefixes") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("MutedUsers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permission", b => + { + b.HasOne("NadekoBot.Services.Database.Models.Permission", "Next") + .WithOne("Previous") + .HasForeignKey("NadekoBot.Services.Database.Models.Permission", "NextId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlayingStatus", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("RotatingStatusMessages") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.HasOne("NadekoBot.Services.Database.Models.MusicPlaylist") + .WithMany("Songs") + .HasForeignKey("MusicPlaylistId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RaceAnimal", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("RaceAnimals") + .HasForeignKey("BotConfigId"); + }); + } + } +} diff --git a/src/NadekoBot/Migrations/20170110111302_repeater-new.cs b/src/NadekoBot/Migrations/20170110111302_repeater-new.cs new file mode 100644 index 00000000..0decfad8 --- /dev/null +++ b/src/NadekoBot/Migrations/20170110111302_repeater-new.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace NadekoBot.Migrations +{ + public partial class repeaternew : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "GuildRepeater", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ChannelId = table.Column(nullable: false), + GuildConfigId = table.Column(nullable: true), + GuildId = table.Column(nullable: false), + Interval = table.Column(nullable: false), + Message = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_GuildRepeater", x => x.Id); + table.ForeignKey( + name: "FK_GuildRepeater_GuildConfigs_GuildConfigId", + column: x => x.GuildConfigId, + principalTable: "GuildConfigs", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_GuildRepeater_GuildConfigId", + table: "GuildRepeater", + column: "GuildConfigId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "GuildRepeater"); + } + } +} diff --git a/src/NadekoBot/Migrations/20170110180534_protection.Designer.cs b/src/NadekoBot/Migrations/20170110180534_protection.Designer.cs new file mode 100644 index 00000000..ecc134cd --- /dev/null +++ b/src/NadekoBot/Migrations/20170110180534_protection.Designer.cs @@ -0,0 +1,942 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using NadekoBot.Services.Database; +using NadekoBot.Services.Database.Models; +using NadekoBot.Modules.Music.Classes; + +namespace NadekoBot.Migrations +{ + [DbContext(typeof(NadekoContext))] + [Migration("20170110180534_protection")] + partial class protection + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.1.0-rtm-22752"); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Action"); + + b.Property("GuildConfigId"); + + b.Property("Seconds"); + + b.Property("UserThreshold"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("AntiRaidSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamIgnore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AntiSpamSettingId"); + + b.Property("ChannelId"); + + b.HasKey("Id"); + + b.HasIndex("AntiSpamSettingId"); + + b.ToTable("AntiSpamIgnore"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Action"); + + b.Property("GuildConfigId"); + + b.Property("MessageThreshold"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("AntiSpamSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlacklistItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("ItemId"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("BlacklistItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BotConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BufferSize"); + + b.Property("CurrencyGenerationChance"); + + b.Property("CurrencyGenerationCooldown"); + + b.Property("CurrencyName"); + + b.Property("CurrencyPluralName"); + + b.Property("CurrencySign"); + + b.Property("DMHelpString"); + + b.Property("ForwardMessages"); + + b.Property("ForwardToAllOwners"); + + b.Property("HelpString"); + + b.Property("MigrationVersion"); + + b.Property("RemindMessageFormat"); + + b.Property("RotatingStatuses"); + + b.HasKey("Id"); + + b.ToTable("BotConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashCaller", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BaseDestroyed"); + + b.Property("CallUser"); + + b.Property("ClashWarId"); + + b.Property("SequenceNumber"); + + b.Property("Stars"); + + b.Property("TimeAdded"); + + b.HasKey("Id"); + + b.HasIndex("ClashWarId"); + + b.ToTable("ClashCallers"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashWar", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("EnemyClan"); + + b.Property("GuildId"); + + b.Property("Size"); + + b.Property("StartedAt"); + + b.Property("WarState"); + + b.HasKey("Id"); + + b.ToTable("ClashOfClans"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CommandName"); + + b.Property("GuildConfigId"); + + b.Property("Seconds"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("CommandCooldown"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ConvertUnit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("InternalTrigger"); + + b.Property("Modifier"); + + b.Property("UnitType"); + + b.HasKey("Id"); + + b.ToTable("ConversionUnits"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Currency"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CurrencyTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("Reason"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.ToTable("CurrencyTransactions"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CustomReaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("GuildId"); + + b.Property("IsRegex"); + + b.Property("OwnerOnly"); + + b.Property("Response"); + + b.Property("Trigger"); + + b.HasKey("Id"); + + b.ToTable("CustomReactions"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Donator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Amount"); + + b.Property("Name"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Donators"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.EightBallResponse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("Text"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("EightBallResponses"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("GuildConfigId"); + + b.Property("GuildConfigId1"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.HasIndex("GuildConfigId1"); + + b.ToTable("FilterChannelId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("GuildConfigId"); + + b.Property("Word"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FilteredWord"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FollowedStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("GuildConfigId"); + + b.Property("GuildId"); + + b.Property("Type"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FollowedStream"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("GuildConfigId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("GCChannelId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AutoAssignRoleId"); + + b.Property("AutoDeleteByeMessages"); + + b.Property("AutoDeleteByeMessagesTimer"); + + b.Property("AutoDeleteGreetMessages"); + + b.Property("AutoDeleteGreetMessagesTimer"); + + b.Property("AutoDeleteSelfAssignedRoleMessages"); + + b.Property("ByeMessageChannelId"); + + b.Property("ChannelByeMessageText"); + + b.Property("ChannelGreetMessageText"); + + b.Property("CleverbotEnabled"); + + b.Property("DefaultMusicVolume"); + + b.Property("DeleteMessageOnCommand"); + + b.Property("DmGreetMessageText"); + + b.Property("ExclusiveSelfAssignedRoles"); + + b.Property("FilterInvites"); + + b.Property("FilterWords"); + + b.Property("GreetMessageChannelId"); + + b.Property("GuildId"); + + b.Property("LogSettingId"); + + b.Property("MuteRoleName"); + + b.Property("PermissionRole"); + + b.Property("RootPermissionId"); + + b.Property("SendChannelByeMessage"); + + b.Property("SendChannelGreetMessage"); + + b.Property("SendDmGreetMessage"); + + b.Property("VerbosePermissions"); + + b.Property("VoicePlusTextEnabled"); + + b.HasKey("Id"); + + b.HasIndex("GuildId") + .IsUnique(); + + b.HasIndex("LogSettingId"); + + b.HasIndex("RootPermissionId"); + + b.ToTable("GuildConfigs"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildRepeater", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("GuildConfigId"); + + b.Property("GuildId"); + + b.Property("Interval"); + + b.Property("Message"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("GuildRepeater"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("LogSettingId"); + + b.HasKey("Id"); + + b.HasIndex("LogSettingId"); + + b.ToTable("IgnoredLogChannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("LogSettingId"); + + b.HasKey("Id"); + + b.HasIndex("LogSettingId"); + + b.ToTable("IgnoredVoicePresenceCHannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.LogSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelCreated"); + + b.Property("ChannelCreatedId"); + + b.Property("ChannelDestroyed"); + + b.Property("ChannelDestroyedId"); + + b.Property("ChannelId"); + + b.Property("ChannelUpdated"); + + b.Property("ChannelUpdatedId"); + + b.Property("IsLogging"); + + b.Property("LogOtherId"); + + b.Property("LogUserPresence"); + + b.Property("LogUserPresenceId"); + + b.Property("LogVoicePresence"); + + b.Property("LogVoicePresenceId"); + + b.Property("LogVoicePresenceTTSId"); + + b.Property("MessageDeleted"); + + b.Property("MessageDeletedId"); + + b.Property("MessageUpdated"); + + b.Property("MessageUpdatedId"); + + b.Property("UserBanned"); + + b.Property("UserBannedId"); + + b.Property("UserJoined"); + + b.Property("UserJoinedId"); + + b.Property("UserLeft"); + + b.Property("UserLeftId"); + + b.Property("UserMutedId"); + + b.Property("UserPresenceChannelId"); + + b.Property("UserUnbanned"); + + b.Property("UserUnbannedId"); + + b.Property("UserUpdated"); + + b.Property("UserUpdatedId"); + + b.Property("VoicePresenceChannelId"); + + b.HasKey("Id"); + + b.ToTable("LogSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ModulePrefix", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("ModuleName"); + + b.Property("Prefix"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("ModulePrefixes"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MusicPlaylist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Author"); + + b.Property("AuthorId"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.ToTable("MusicPlaylists"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("GuildConfigId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("MutedUserId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("NextId"); + + b.Property("PrimaryTarget"); + + b.Property("PrimaryTargetId"); + + b.Property("SecondaryTarget"); + + b.Property("SecondaryTargetName"); + + b.Property("State"); + + b.HasKey("Id"); + + b.HasIndex("NextId") + .IsUnique(); + + b.ToTable("Permission"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlayingStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("Status"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("PlayingStatus"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("MusicPlaylistId"); + + b.Property("Provider"); + + b.Property("ProviderType"); + + b.Property("Query"); + + b.Property("Title"); + + b.Property("Uri"); + + b.HasKey("Id"); + + b.HasIndex("MusicPlaylistId"); + + b.ToTable("PlaylistSong"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Quote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuthorId"); + + b.Property("AuthorName") + .IsRequired(); + + b.Property("GuildId"); + + b.Property("Keyword") + .IsRequired(); + + b.Property("Text") + .IsRequired(); + + b.HasKey("Id"); + + b.ToTable("Quotes"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RaceAnimal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("BotConfigId"); + + b.Property("Icon"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.HasIndex("BotConfigId"); + + b.ToTable("RaceAnimals"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Reminder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("IsPrivate"); + + b.Property("Message"); + + b.Property("ServerId"); + + b.Property("UserId"); + + b.Property("When"); + + b.HasKey("Id"); + + b.ToTable("Reminders"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SelfAssignedRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("GuildId"); + + b.Property("RoleId"); + + b.HasKey("Id"); + + b.HasIndex("GuildId", "RoleId") + .IsUnique(); + + b.ToTable("SelfAssignableRoles"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UserPokeTypes", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("UserId"); + + b.Property("type"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("PokeGame"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("AntiRaidSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiRaidSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamIgnore", b => + { + b.HasOne("NadekoBot.Services.Database.Models.AntiSpamSetting") + .WithMany("IgnoredChannels") + .HasForeignKey("AntiSpamSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("AntiSpamSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiSpamSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlacklistItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("Blacklist") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ClashCaller", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ClashWar", "ClashWar") + .WithMany("Bases") + .HasForeignKey("ClashWarId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("CommandCooldowns") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.EightBallResponse", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("EightBallResponses") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilterInvitesChannelIds") + .HasForeignKey("GuildConfigId"); + + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilterWordsChannelIds") + .HasForeignKey("GuildConfigId1"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FilteredWords") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FollowedStream", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("FollowedStreams") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("GenerateCurrencyChannelIds") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany() + .HasForeignKey("LogSettingId"); + + b.HasOne("NadekoBot.Services.Database.Models.Permission", "RootPermission") + .WithMany() + .HasForeignKey("RootPermissionId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildRepeater", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("GuildRepeaters") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany("IgnoredChannels") + .HasForeignKey("LogSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany("IgnoredVoicePresenceChannelIds") + .HasForeignKey("LogSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ModulePrefix", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("ModulePrefixes") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("MutedUsers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permission", b => + { + b.HasOne("NadekoBot.Services.Database.Models.Permission", "Next") + .WithOne("Previous") + .HasForeignKey("NadekoBot.Services.Database.Models.Permission", "NextId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlayingStatus", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("RotatingStatusMessages") + .HasForeignKey("BotConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.HasOne("NadekoBot.Services.Database.Models.MusicPlaylist") + .WithMany("Songs") + .HasForeignKey("MusicPlaylistId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RaceAnimal", b => + { + b.HasOne("NadekoBot.Services.Database.Models.BotConfig") + .WithMany("RaceAnimals") + .HasForeignKey("BotConfigId"); + }); + } + } +} diff --git a/src/NadekoBot/Migrations/20170110180534_protection.cs b/src/NadekoBot/Migrations/20170110180534_protection.cs new file mode 100644 index 00000000..fca7f541 --- /dev/null +++ b/src/NadekoBot/Migrations/20170110180534_protection.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace NadekoBot.Migrations +{ + public partial class protection : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AntiRaidSetting", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Action = table.Column(nullable: false), + GuildConfigId = table.Column(nullable: false), + Seconds = table.Column(nullable: false), + UserThreshold = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AntiRaidSetting", x => x.Id); + table.ForeignKey( + name: "FK_AntiRaidSetting_GuildConfigs_GuildConfigId", + column: x => x.GuildConfigId, + principalTable: "GuildConfigs", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "AntiSpamSetting", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Action = table.Column(nullable: false), + GuildConfigId = table.Column(nullable: false), + MessageThreshold = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AntiSpamSetting", x => x.Id); + table.ForeignKey( + name: "FK_AntiSpamSetting_GuildConfigs_GuildConfigId", + column: x => x.GuildConfigId, + principalTable: "GuildConfigs", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "AntiSpamIgnore", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + AntiSpamSettingId = table.Column(nullable: true), + ChannelId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AntiSpamIgnore", x => x.Id); + table.ForeignKey( + name: "FK_AntiSpamIgnore_AntiSpamSetting_AntiSpamSettingId", + column: x => x.AntiSpamSettingId, + principalTable: "AntiSpamSetting", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_AntiRaidSetting_GuildConfigId", + table: "AntiRaidSetting", + column: "GuildConfigId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_AntiSpamIgnore_AntiSpamSettingId", + table: "AntiSpamIgnore", + column: "AntiSpamSettingId"); + + migrationBuilder.CreateIndex( + name: "IX_AntiSpamSetting_GuildConfigId", + table: "AntiSpamSetting", + column: "GuildConfigId", + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AntiRaidSetting"); + + migrationBuilder.DropTable( + name: "AntiSpamIgnore"); + + migrationBuilder.DropTable( + name: "AntiSpamSetting"); + } + } +} diff --git a/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs b/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs index c9b359e1..190e425e 100644 --- a/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs +++ b/src/NadekoBot/Migrations/NadekoSqliteContextModelSnapshot.cs @@ -2,7 +2,10 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; using NadekoBot.Services.Database; +using NadekoBot.Services.Database.Models; +using NadekoBot.Modules.Music.Classes; namespace NadekoBot.Migrations { @@ -14,6 +17,62 @@ namespace NadekoBot.Migrations modelBuilder .HasAnnotation("ProductVersion", "1.1.0-rtm-22752"); + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Action"); + + b.Property("GuildConfigId"); + + b.Property("Seconds"); + + b.Property("UserThreshold"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("AntiRaidSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamIgnore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AntiSpamSettingId"); + + b.Property("ChannelId"); + + b.HasKey("Id"); + + b.HasIndex("AntiSpamSettingId"); + + b.ToTable("AntiSpamIgnore"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Action"); + + b.Property("GuildConfigId"); + + b.Property("MessageThreshold"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("AntiSpamSetting"); + }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlacklistItem", b => { b.Property("Id") @@ -381,6 +440,28 @@ namespace NadekoBot.Migrations b.ToTable("GuildConfigs"); }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildRepeater", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChannelId"); + + b.Property("GuildConfigId"); + + b.Property("GuildId"); + + b.Property("Interval"); + + b.Property("Message"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("GuildRepeater"); + }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogChannel", b => { b.Property("Id") @@ -663,27 +744,6 @@ namespace NadekoBot.Migrations b.ToTable("Reminders"); }); - modelBuilder.Entity("NadekoBot.Services.Database.Models.Repeater", b => - { - b.Property("Id") - .ValueGeneratedOnAdd(); - - b.Property("ChannelId"); - - b.Property("GuildId"); - - b.Property("Interval"); - - b.Property("Message"); - - b.HasKey("Id"); - - b.HasIndex("ChannelId") - .IsUnique(); - - b.ToTable("Repeaters"); - }); - modelBuilder.Entity("NadekoBot.Services.Database.Models.SelfAssignedRole", b => { b.Property("Id") @@ -718,6 +778,29 @@ namespace NadekoBot.Migrations b.ToTable("PokeGame"); }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("AntiRaidSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiRaidSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamIgnore", b => + { + b.HasOne("NadekoBot.Services.Database.Models.AntiSpamSetting") + .WithMany("IgnoredChannels") + .HasForeignKey("AntiSpamSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("AntiSpamSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiSpamSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade); + }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlacklistItem", b => { b.HasOne("NadekoBot.Services.Database.Models.BotConfig") @@ -790,6 +873,13 @@ namespace NadekoBot.Migrations .HasForeignKey("RootPermissionId"); }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildRepeater", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig") + .WithMany("GuildRepeaters") + .HasForeignKey("GuildConfigId"); + }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogChannel", b => { b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") diff --git a/src/NadekoBot/Modules/Administration/Administration.cs b/src/NadekoBot/Modules/Administration/Administration.cs index 91c952a6..cf431be1 100644 --- a/src/NadekoBot/Modules/Administration/Administration.cs +++ b/src/NadekoBot/Modules/Administration/Administration.cs @@ -35,7 +35,7 @@ namespace NadekoBot.Modules.Administration NadekoBot.CommandHandler.CommandExecuted += DelMsgOnCmd_Handler; DeleteMessagesOnCommand = new ConcurrentHashSet(NadekoBot.AllGuildConfigs.Where(g => g.DeleteMessageOnCommand).Select(g => g.GuildId)); - + } private static async Task DelMsgOnCmd_Handler(SocketUserMessage msg, CommandInfo cmd) @@ -202,7 +202,7 @@ namespace NadekoBot.Modules.Administration return; } var roleName = args[0].ToUpperInvariant(); - var role = Context.Guild.Roles.Where(r=>r.Name.ToUpperInvariant() == roleName).FirstOrDefault(); + var role = Context.Guild.Roles.Where(r => r.Name.ToUpperInvariant() == roleName).FirstOrDefault(); if (role == null) { @@ -217,7 +217,7 @@ namespace NadekoBot.Modules.Administration var red = Convert.ToByte(rgb ? int.Parse(arg1) : Convert.ToInt32(arg1.Substring(0, 2), 16)); var green = Convert.ToByte(rgb ? int.Parse(args[2]) : Convert.ToInt32(arg1.Substring(2, 2), 16)); var blue = Convert.ToByte(rgb ? int.Parse(args[3]) : Convert.ToInt32(arg1.Substring(4, 2), 16)); - + await role.ModifyAsync(r => r.Color = new Color(red, green, blue)).ConfigureAwait(false); await Context.Channel.SendConfirmAsync($"☑️ Role **{role.Name}'s** color has been changed.").ConfigureAwait(false); } @@ -236,18 +236,22 @@ namespace NadekoBot.Modules.Administration { msg = "❗️No reason provided."; } - if (Context.User.Id != user.Guild.OwnerId && ((IGuildUser)Context.User).GetRoles().Select(r=>r.Position).Max() >= ((IGuildUser)Context.User).GetRoles().Select(r => r.Position).Max()) + if (Context.User.Id != user.Guild.OwnerId && (user.GetRoles().Select(r => r.Position).Max() >= ((IGuildUser)Context.User).GetRoles().Select(r => r.Position).Max())) { await Context.Channel.SendErrorAsync("⚠️ You can't use this command on users with a role higher or equal to yours in the role hierarchy.").ConfigureAwait(false); return; } - try + if (!string.IsNullOrWhiteSpace(msg)) { - await (await user.CreateDMChannelAsync()).SendErrorAsync($"⛔️ **You have been BANNED from `{Context.Guild.Name}` server.**\n" + - $"⚖ *Reason:* {msg}").ConfigureAwait(false); - await Task.Delay(2000).ConfigureAwait(false); + try + { + await (await user.CreateDMChannelAsync()).SendErrorAsync($"⛔️ **You have been BANNED from `{Context.Guild.Name}` server.**\n" + + $"⚖ *Reason:* {msg}").ConfigureAwait(false); + await Task.Delay(2000).ConfigureAwait(false); + + } + catch { } } - catch { } try { await Context.Guild.AddBanAsync(user, 7).ConfigureAwait(false); @@ -275,13 +279,18 @@ namespace NadekoBot.Modules.Administration await Context.Channel.SendErrorAsync("⚠️ You can't use this command on users with a role higher or equal to yours in the role hierarchy."); return; } - try + + if (!string.IsNullOrWhiteSpace(msg)) { - await user.SendErrorAsync($"☣ **You have been SOFT-BANNED from `{Context.Guild.Name}` server.**\n" + - $"⚖ *Reason:* {msg}").ConfigureAwait(false); - await Task.Delay(2000).ConfigureAwait(false); + try + { + await user.SendErrorAsync($"☣ **You have been SOFT-BANNED from `{Context.Guild.Name}` server.**\n" + + $"⚖ *Reason:* {msg}").ConfigureAwait(false); + await Task.Delay(2000).ConfigureAwait(false); + } + catch { } } - catch { } + try { await Context.Guild.AddBanAsync(user, 7).ConfigureAwait(false); @@ -439,7 +448,7 @@ namespace NadekoBot.Modules.Administration public async Task Prune() { var user = await Context.Guild.GetCurrentUserAsync().ConfigureAwait(false); - + var enumerable = (await Context.Channel.GetMessagesAsync().Flatten()).AsEnumerable(); enumerable = enumerable.Where(x => x.Author.Id == user.Id); await Context.Channel.DeleteMessagesAsync(enumerable).ConfigureAwait(false); @@ -451,6 +460,9 @@ namespace NadekoBot.Modules.Administration [RequireUserPermission(ChannelPermission.ManageMessages)] public async Task Prune(int count) { + if (count < 1) + return; + count += 1; await Context.Message.DeleteAsync().ConfigureAwait(false); int limit = (count < 100) ? count : 100; var enumerable = (await Context.Channel.GetMessagesAsync(limit: limit).Flatten().ConfigureAwait(false)); @@ -463,45 +475,17 @@ namespace NadekoBot.Modules.Administration [RequireUserPermission(ChannelPermission.ManageMessages)] public async Task Prune(IGuildUser user, int count = 100) { + if (count < 1) + return; + + if (user.Id == Context.User.Id) + count += 1; int limit = (count < 100) ? count : 100; var enumerable = (await Context.Channel.GetMessagesAsync(limit: limit).Flatten()).Where(m => m.Author == user); await Context.Channel.DeleteMessagesAsync(enumerable).ConfigureAwait(false); } - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - [OwnerOnly] - public async Task SaveChat(int cnt) - { - ulong? lastmsgId = null; - var sb = new StringBuilder(); - var msgs = new List(cnt); - while (cnt > 0) - { - var dlcnt = cnt < 100 ? cnt : 100; - IEnumerable dledMsgs; - if (lastmsgId == null) - dledMsgs = await Context.Channel.GetMessagesAsync(cnt).Flatten().ConfigureAwait(false); - else - dledMsgs = await Context.Channel.GetMessagesAsync(lastmsgId.Value, Direction.Before, dlcnt).Flatten().ConfigureAwait(false); - - if (!dledMsgs.Any()) - break; - - msgs.AddRange(dledMsgs); - lastmsgId = msgs[msgs.Count - 1].Id; - cnt -= 100; - } - var title = $"Chatlog-{Context.Guild.Name}/#{Context.Channel.Name}-{DateTime.Now}.txt"; - var grouping = msgs.GroupBy(x => $"{x.CreatedAt.Date:dd.MM.yyyy}") - .Select(g => new { date = g.Key, messages = g.OrderBy(x => x.CreatedAt).Select(s => $"【{s.Timestamp:HH:mm:ss}】{s.Author}:" + s.ToString()) }); - await (Context.User as IGuildUser).SendFileAsync( - await JsonConvert.SerializeObject(grouping, Formatting.Indented).ToStream().ConfigureAwait(false), -title, title).ConfigureAwait(false); - } - - [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.MentionEveryone)] @@ -511,7 +495,7 @@ title, title).ConfigureAwait(false); foreach (var role in roles) { send += $"\n**{role.Name}**\n"; - send += string.Join(", ", (await Context.Guild.GetUsersAsync()).Where(u => u.GetRoles().Contains(role)).Distinct().Select(u=>u.Mention)); + send += string.Join(", ", (await Context.Guild.GetUsersAsync()).Where(u => u.GetRoles().Contains(role)).Distinct().Select(u => u.Mention)); } while (send.Length > 2000) @@ -536,7 +520,7 @@ title, title).ConfigureAwait(false); donatorsOrdered = uow.Donators.GetDonatorsOrdered(); } await Context.Channel.SendConfirmAsync("Thanks to the people listed below for making this project happen!", string.Join("⭐", donatorsOrdered.Select(d => d.Name))).ConfigureAwait(false); - + nadekoSupportServer = nadekoSupportServer ?? NadekoBot.Client.GetGuild(117523346618318850); if (nadekoSupportServer == null) diff --git a/src/NadekoBot/Modules/Administration/Commands/AntiRaidCommands.cs b/src/NadekoBot/Modules/Administration/Commands/AntiRaidCommands.cs deleted file mode 100644 index 4551ef48..00000000 --- a/src/NadekoBot/Modules/Administration/Commands/AntiRaidCommands.cs +++ /dev/null @@ -1,272 +0,0 @@ -using Discord; -using Discord.Commands; -using NadekoBot.Attributes; -using NadekoBot.Extensions; -using NLog; -using System; -using System.Collections.Concurrent; -using System.Linq; -using System.Threading.Tasks; - -namespace NadekoBot.Modules.Administration -{ - public partial class Administration - { - public enum PunishmentAction - { - Mute, - Kick, - Ban, - } - - public enum ProtectionType - { - Raiding, - Spamming, - } - - private class AntiRaidSetting - { - public int UserThreshold { get; set; } - public int Seconds { get; set; } - public PunishmentAction Action { get; set; } - public int UsersCount { get; set; } - public ConcurrentHashSet RaidUsers { get; set; } = new ConcurrentHashSet(); - } - - private class AntiSpamSetting - { - public PunishmentAction Action { get; set; } - public int MessageThreshold { get; set; } = 3; - public ConcurrentDictionary UserStats { get; set; } - = new ConcurrentDictionary(); - } - - private class UserSpamStats - { - public int Count { get; set; } - public string LastMessage { get; set; } - - public UserSpamStats(string msg) - { - Count = 1; - LastMessage = msg.ToUpperInvariant(); - } - - public void ApplyNextMessage(string message) - { - var upperMsg = message.ToUpperInvariant(); - if (upperMsg == LastMessage) - Count++; - else - { - LastMessage = upperMsg; - Count = 0; - } - } - } - - [Group] - public class AntiRaidCommands : ModuleBase - { - private static ConcurrentDictionary antiRaidGuilds = - new ConcurrentDictionary(); - // guildId | (userId|messages) - private static ConcurrentDictionary antiSpamGuilds = - new ConcurrentDictionary(); - - private static Logger _log { get; } - - static AntiRaidCommands() - { - _log = LogManager.GetCurrentClassLogger(); - - NadekoBot.Client.MessageReceived += async (imsg) => - { - - try - { - var msg = imsg as IUserMessage; - if (msg == null || msg.Author.IsBot) - return; - - var channel = msg.Channel as ITextChannel; - if (channel == null) - return; - AntiSpamSetting spamSettings; - if (!antiSpamGuilds.TryGetValue(channel.Guild.Id, out spamSettings)) - return; - - var stats = spamSettings.UserStats.AddOrUpdate(msg.Author.Id, new UserSpamStats(msg.Content), - (id, old) => { old.ApplyNextMessage(msg.Content); return old; }); - - if (stats.Count >= spamSettings.MessageThreshold) - { - if (spamSettings.UserStats.TryRemove(msg.Author.Id, out stats)) - { - await PunishUsers(spamSettings.Action, ProtectionType.Spamming, (IGuildUser)msg.Author) - .ConfigureAwait(false); - } - } - } - catch { } - }; - - NadekoBot.Client.UserJoined += async (usr) => - { - try - { - if (usr.IsBot) - return; - AntiRaidSetting settings; - if (!antiRaidGuilds.TryGetValue(usr.Guild.Id, out settings)) - return; - if (!settings.RaidUsers.Add(usr)) - return; - - ++settings.UsersCount; - - if (settings.UsersCount >= settings.UserThreshold) - { - var users = settings.RaidUsers.ToArray(); - settings.RaidUsers.Clear(); - - await PunishUsers(settings.Action, ProtectionType.Raiding, users).ConfigureAwait(false); - } - await Task.Delay(1000 * settings.Seconds).ConfigureAwait(false); - - settings.RaidUsers.TryRemove(usr); - --settings.UsersCount; - - } - catch { } - }; - } - - private static async Task PunishUsers(PunishmentAction action, ProtectionType pt, params IGuildUser[] gus) - { - foreach (var gu in gus) - { - switch (action) - { - case PunishmentAction.Mute: - try - { - await MuteCommands.MuteUser(gu).ConfigureAwait(false); - } - catch (Exception ex) { _log.Warn(ex, "I can't apply punishement"); } - break; - case PunishmentAction.Kick: - try - { - await gu.Guild.AddBanAsync(gu, 7).ConfigureAwait(false); - try - { - await gu.Guild.RemoveBanAsync(gu).ConfigureAwait(false); - } - catch - { - await gu.Guild.RemoveBanAsync(gu).ConfigureAwait(false); - // try it twice, really don't want to ban user if - // only kick has been specified as the punishement - } - } - catch (Exception ex) { _log.Warn(ex, "I can't apply punishment"); } - break; - case PunishmentAction.Ban: - try - { - await gu.Guild.AddBanAsync(gu, 7).ConfigureAwait(false); - } - catch (Exception ex) { _log.Warn(ex, "I can't apply punishment"); } - break; - default: - break; - } - } - await LogCommands.TriggeredAntiProtection(gus, action, pt).ConfigureAwait(false); - } - - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - [RequireUserPermission(GuildPermission.Administrator)] - public async Task AntiRaid(int userThreshold, int seconds, PunishmentAction action) - { - if (userThreshold < 2 || userThreshold > 30) - { - await Context.Channel.SendErrorAsync("❗️User threshold must be between **2** and **30**.").ConfigureAwait(false); - return; - } - - if (seconds < 2 || seconds > 300) - { - await Context.Channel.SendErrorAsync("❗️Time must be between **2** and **300** seconds.").ConfigureAwait(false); - return; - } - - try - { - await MuteCommands.GetMuteRole(Context.Guild).ConfigureAwait(false); - } - catch (Exception ex) - { - await Context.Channel.SendConfirmAsync("⚠️ Failed creating a mute role. Give me ManageRoles permission" + - "or create 'nadeko-mute' role with disabled SendMessages and try again.") - .ConfigureAwait(false); - _log.Warn(ex); - return; - } - - var setting = new AntiRaidSetting() - { - Action = action, - Seconds = seconds, - UserThreshold = userThreshold, - }; - antiRaidGuilds.AddOrUpdate(Context.Guild.Id, setting, (id, old) => setting); - - await Context.Channel.SendConfirmAsync($"ℹ️ {Context.User.Mention} If **{userThreshold}** or more users join within **{seconds}** seconds, I will **{action}** them.") - .ConfigureAwait(false); - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - [RequireUserPermission(GuildPermission.Administrator)] - public async Task AntiSpam(int messageCount=3, PunishmentAction action = PunishmentAction.Mute) - { - if (messageCount < 2 || messageCount > 10) - return; - - AntiSpamSetting throwaway; - if (antiSpamGuilds.TryRemove(Context.Guild.Id, out throwaway)) - { - await Context.Channel.SendConfirmAsync("🆗 **Anti-Spam feature** has been **disabled** on this server.").ConfigureAwait(false); - } - else - { - try - { - await MuteCommands.GetMuteRole(Context.Guild).ConfigureAwait(false); - } - catch (Exception ex) - { - await Context.Channel.SendErrorAsync("⚠️ Failed creating a mute role. Give me ManageRoles permission" + - "or create 'nadeko-mute' role with disabled SendMessages and try again.") - .ConfigureAwait(false); - _log.Warn(ex); - return; - } - - if (antiSpamGuilds.TryAdd(Context.Guild.Id, new AntiSpamSetting() - { - Action = action, - MessageThreshold = messageCount, - })) - await Context.Channel.SendConfirmAsync("✅ **Anti-Spam feature** has been **enabled** on this server.").ConfigureAwait(false); - } - - } - } - } -} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/Commands/LogCommand.cs b/src/NadekoBot/Modules/Administration/Commands/LogCommand.cs index 04bde59b..c7e5d513 100644 --- a/src/NadekoBot/Modules/Administration/Commands/LogCommand.cs +++ b/src/NadekoBot/Modules/Administration/Commands/LogCommand.cs @@ -23,14 +23,17 @@ namespace NadekoBot.Modules.Administration [Group] public class LogCommands : ModuleBase { + private const string clockEmojiUrl = "https://cdn.discordapp.com/attachments/155726317222887425/258309524966866945/clock.png"; + private static ShardedDiscordClient _client { get; } private static Logger _log { get; } private static string prettyCurrentTime => $"【{DateTime.Now:HH:mm:ss}】"; + private static string currentTime = $"{DateTime.Now:HH:mm:ss}"; public static ConcurrentDictionary GuildLogSettings { get; } - private static ConcurrentDictionary> UserPresenceUpdates { get; } = new ConcurrentDictionary>(); + private static ConcurrentDictionary> PresenceUpdates { get; } = new ConcurrentDictionary>(); private static Timer timerReference { get; } private IGoogleApiService _google { get; } @@ -50,20 +53,20 @@ namespace NadekoBot.Modules.Administration { try { - var keys = UserPresenceUpdates.Keys.ToList(); + var keys = PresenceUpdates.Keys.ToList(); await Task.WhenAll(keys.Select(async key => { List messages; - if (UserPresenceUpdates.TryRemove(key, out messages)) - try { await key.SendMessageAsync(string.Join(Environment.NewLine, messages)); } catch { } + if (PresenceUpdates.TryRemove(key, out messages)) + try { await key.SendConfirmAsync("Presence Updates", string.Join(Environment.NewLine, messages)); } catch { } })); } catch (Exception ex) { _log.Warn(ex); } - }, null, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(10)); + }, null, TimeSpan.FromSeconds(15), TimeSpan.FromSeconds(15)); sw.Stop(); _log.Debug($"Loaded in {sw.Elapsed.TotalSeconds:F2}s"); @@ -78,7 +81,10 @@ namespace NadekoBot.Modules.Administration _client.UserPresenceUpdated += _client_UserPresenceUpdated; _client.UserVoiceStateUpdated += _client_UserVoiceStateUpdated; _client.UserVoiceStateUpdated += _client_UserVoiceStateUpdated_TTS; + _client.GuildUserUpdated += _client_GuildUserUpdated; +#if !GLOBAL_NADEKO _client.UserUpdated += _client_UserUpdated; +#endif _client.ChannelCreated += _client_ChannelCreated; _client.ChannelDestroyed += _client_ChannelDestroyed; @@ -88,6 +94,74 @@ namespace NadekoBot.Modules.Administration MuteCommands.UserUnmuted += MuteCommands_UserUnmuted; } + private static async void _client_UserUpdated(SocketUser before, SocketUser uAfter) + { + try + { + var after = uAfter as SocketGuildUser; + + if (after == null) + return; + + var g = after.Guild; + + LogSetting logSetting; + if (!GuildLogSettings.TryGetValue(g.Id, out logSetting) + || (logSetting.UserUpdatedId == null)) + return; + + ITextChannel logChannel; + if ((logChannel = await TryGetLogChannel(g, logSetting, LogType.UserUpdated)) == null) + return; + + var embed = new EmbedBuilder(); + + + if (before.Username != after.Username) + { + embed.WithTitle("👥 Username Changed") + .WithDescription($"{before.Username}#{before.Discriminator} | {before.Id}") + .AddField(fb => fb.WithName("Old Name").WithValue($"{before.Username}").WithIsInline(true)) + .AddField(fb => fb.WithName("New Name").WithValue($"{after.Username}").WithIsInline(true)) + .WithFooter(fb => fb.WithText(currentTime)) + .WithOkColor(); + } + else if (before.AvatarUrl != after.AvatarUrl) + { + embed.WithTitle("👥 Avatar Changed") + .WithDescription($"{before.Username}#{before.Discriminator} | {before.Id}") + .WithTitle($"{before.Username}#{before.Discriminator} | {before.Id}") + .WithThumbnailUrl(before.AvatarUrl) + .WithImageUrl(after.AvatarUrl) + .WithFooter(fb => fb.WithText(currentTime)) + .WithOkColor(); + } + else + { + return; + } + + await logChannel.EmbedAsync(embed).ConfigureAwait(false); + + //var guildsMemberOf = NadekoBot.Client.GetGuilds().Where(g => g.Users.Select(u => u.Id).Contains(before.Id)).ToList(); + //foreach (var g in guildsMemberOf) + //{ + // LogSetting logSetting; + // if (!GuildLogSettings.TryGetValue(g.Id, out logSetting) + // || (logSetting.UserUpdatedId == null)) + // return; + + // ITextChannel logChannel; + // if ((logChannel = await TryGetLogChannel(g, logSetting, LogType.UserUpdated)) == null) + // return; + + // try { await logChannel.SendMessageAsync(str).ConfigureAwait(false); } catch { } + //} + } + catch + { } + } + private static async void _client_UserVoiceStateUpdated_TTS(SocketUser iusr, SocketVoiceState before, SocketVoiceState after) { try @@ -111,7 +185,7 @@ namespace NadekoBot.Modules.Administration if ((logChannel = await TryGetLogChannel(usr.Guild, logSetting, LogType.VoicePresenceTTS)) == null) return; - string str = null; + var str = ""; if (beforeVch?.Guild == afterVch?.Guild) { str = $"{usr.Username} moved from {beforeVch.Name} to {afterVch.Name}"; @@ -155,9 +229,15 @@ namespace NadekoBot.Modules.Administration mutes = "text and voice chat"; break; } - await logChannel.SendMessageAsync($"‼️🕕`{prettyCurrentTime}`👤__**{usr.Username}#{usr.Discriminator}**__🔇 **| User muted from the {mutes}. |** 🆔 `{usr.Id}`").ConfigureAwait(false); + + var embed = new EmbedBuilder().WithAuthor(eab => eab.WithName("🔇 User Muted from " + mutes)) + .WithTitle($"{usr.Username}#{usr.Discriminator} | {usr.Id}") + .WithFooter(fb => fb.WithText(currentTime)) + .WithOkColor(); + + await logChannel.EmbedAsync(embed).ConfigureAwait(false); } - catch (Exception ex) { _log.Warn(ex); } + catch { } } private static async void MuteCommands_UserUnmuted(IGuildUser usr, MuteCommands.MuteType muteType) @@ -186,9 +266,15 @@ namespace NadekoBot.Modules.Administration mutes = "text and voice chat"; break; } - await logChannel.SendMessageAsync($"‼️🕕`{prettyCurrentTime}`👤__**{usr.Username}#{usr.Discriminator}**__🔊 **| User unmuted from the {mutes}. |** 🆔 `{usr.Id}`").ConfigureAwait(false); + + var embed = new EmbedBuilder().WithAuthor(eab => eab.WithName("🔊 User Unmuted from " + mutes)) + .WithTitle($"{usr.Username}#{usr.Discriminator} | {usr.Id}") + .WithFooter(fb => fb.WithText($"{currentTime}")) + .WithOkColor(); + + await logChannel.EmbedAsync(embed).ConfigureAwait(false); } - catch (Exception ex) { _log.Warn(ex); } + catch { } } public static async Task TriggeredAntiProtection(IGuildUser[] users, PunishmentAction action, ProtectionType protection) @@ -210,35 +296,31 @@ namespace NadekoBot.Modules.Administration if (action == PunishmentAction.Mute) { punishment = "🔇 MUTED"; - //punishment = "MUTED"; } else if (action == PunishmentAction.Kick) { punishment = "☣ SOFT-BANNED (KICKED)"; - //punishment = "KICKED"; } else if (action == PunishmentAction.Ban) { punishment = "⛔️ BANNED"; - //punishment = "BANNED"; } - await logChannel.SendMessageAsync(String.Join("\n", users.Select(user => $"‼️ {Format.Bold(user.ToString())} got **{punishment}** due to __**{protection}**__ protection on **{user.Guild.Name}** server."))) - .ConfigureAwait(false); + + var embed = new EmbedBuilder().WithAuthor(eab => eab.WithName($"🛡 Anti-{protection}")) + .WithTitle($"Users " + punishment) + .WithDescription(String.Join("\n", users.Select(u => u.ToString()))) + .WithFooter(fb => fb.WithText($"{currentTime}")) + .WithOkColor(); + + await logChannel.EmbedAsync(embed).ConfigureAwait(false); } - catch (Exception ex) { _log.Warn(ex); } + catch { } } - private static async void _client_UserUpdated(SocketUser uBefore, SocketUser uAfter) + private static async void _client_GuildUserUpdated(SocketGuildUser before, SocketGuildUser after) { try { - var before = uBefore as SocketGuildUser; - if (before == null) - return; - var after = uAfter as SocketGuildUser; - if (after == null) - return; - LogSetting logSetting; if (!GuildLogSettings.TryGetValue(before.Guild.Id, out logSetting) || (logSetting.UserUpdatedId == null)) @@ -247,32 +329,35 @@ namespace NadekoBot.Modules.Administration ITextChannel logChannel; if ((logChannel = await TryGetLogChannel(before.Guild, logSetting, LogType.UserUpdated)) == null) return; - string str = $"🕔`{prettyCurrentTime}`"; + var embed = new EmbedBuilder().WithOkColor().WithFooter(efb => efb.WithText(currentTime)) + .WithTitle($"{before.Username}#{before.Discriminator} | {before.Id}"); + if (before.Nickname != after.Nickname) + { + embed.WithAuthor(eab => eab.WithName("👥 Nickname Changed")) - if (before.Username != after.Username) - str += $"👤__**{before.Username}#{before.Discriminator}**__ **| Name Changed |** 🆔 `{before.Id}`\n\t\t`New:` **{after.ToString()}**"; - else if (before.Nickname != after.Nickname) - str += $"👤__**{before.Username}#{before.Discriminator}**__ **| Nickname Changed |** 🆔 `{before.Id}`\n\t\t`Old:` **{before.Nickname}#{before.Discriminator}**\n\t\t`New:` **{after.Nickname}#{after.Discriminator}**"; - else if (before.AvatarUrl != after.AvatarUrl) - str += $"👤__**{before.Username}#{before.Discriminator}**__ **| Avatar Changed |** 🆔 `{before.Id}`\n\t🖼 {await NadekoBot.Google.ShortenUrl(before.AvatarUrl)} `=>` {await NadekoBot.Google.ShortenUrl(after.AvatarUrl)}"; + .AddField(efb => efb.WithName("Old Nickname").WithValue($"{before.Nickname}#{before.Discriminator}")) + .AddField(efb => efb.WithName("New Nickname").WithValue($"{after.Nickname}#{after.Discriminator}")); + } else if (!before.RoleIds.SequenceEqual(after.RoleIds)) { if (before.RoleIds.Count < after.RoleIds.Count) { - var diffRoles = after.RoleIds.Where(r => !before.RoleIds.Contains(r)).Select(r => "**" + before.Guild.GetRole(r).Name + "**"); - str += $"👤__**{before.ToString()}**__ **| User's Role Added |** 🆔 `{before.Id}`\n\t✅ {string.Join(", ", diffRoles).SanitizeMentions()}\n\t\t⚔ **`{string.Join(", ", after.GetRoles().Select(r => r.Name)).SanitizeMentions()}`** ⚔"; + var diffRoles = after.RoleIds.Where(r => !before.RoleIds.Contains(r)).Select(r => before.Guild.GetRole(r).Name); + embed.WithAuthor(eab => eab.WithName("⚔ User's Role Added")) + .WithDescription(string.Join(", ", diffRoles).SanitizeMentions()); } else if (before.RoleIds.Count > after.RoleIds.Count) { - var diffRoles = before.RoleIds.Where(r => !after.RoleIds.Contains(r)).Select(r => "**" + before.Guild.GetRole(r).Name + "**"); - str += $"👤__**{before.ToString()}**__ **| User's Role Removed |** 🆔 `{before.Id}`\n\t🚮 {string.Join(", ", diffRoles).SanitizeMentions()}\n\t\t⚔ **`{string.Join(", ", after.GetRoles().Select(r => r.Name)).SanitizeMentions()}`** ⚔"; + var diffRoles = before.RoleIds.Where(r => !after.RoleIds.Contains(r)).Select(r => before.Guild.GetRole(r).Name); + embed.WithAuthor(eab => eab.WithName("⚔ User's Role Removed")) + .WithDescription(string.Join(", ", diffRoles).SanitizeMentions()); } } else return; - try { await logChannel.SendMessageAsync(str).ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); } + await logChannel.EmbedAsync(embed).ConfigureAwait(false); } - catch (Exception ex) { _log.Warn(ex); } + catch { } } private static async void _client_ChannelUpdated(IChannel cbefore, IChannel cafter) @@ -293,16 +378,29 @@ namespace NadekoBot.Modules.Administration ITextChannel logChannel; if ((logChannel = await TryGetLogChannel(before.Guild, logSetting, LogType.ChannelUpdated)) == null) return; + + var embed = new EmbedBuilder().WithOkColor().WithFooter(efb => efb.WithText(currentTime)); + + var beforeTextChannel = cbefore as ITextChannel; + var afterTextChannel = cafter as ITextChannel; + if (before.Name != after.Name) - await logChannel.SendMessageAsync($@"🕓`{prettyCurrentTime}`ℹ️ **| Channel Name Changed |** #⃣ `{after.Name} ({after.Id})` - `Old:` {before.Name} - **`New:`** {after.Name}").ConfigureAwait(false); - else if ((before as ITextChannel).Topic != (after as ITextChannel).Topic) - await logChannel.SendMessageAsync($@"🕘`{prettyCurrentTime}`ℹ️ **| Channel Topic Changed |** #⃣ `{after.Name} ({after.Id})` - `Old:` {((ITextChannel)before).Topic} - **`New:`** {((ITextChannel)after).Topic}").ConfigureAwait(false); + { + embed.WithTitle("ℹ️ Channel Name Changed") + .WithDescription($"{after} | {after.Id}") + .AddField(efb => efb.WithName("Old Name").WithValue(before.Name)); + } + else if (beforeTextChannel?.Topic != afterTextChannel?.Topic) + { + embed.WithTitle("ℹ️ Channel Topic Changed") + .WithDescription($"{after} | {after.Id}") + .AddField(efb => efb.WithName("Old Topic").WithValue(beforeTextChannel.Topic)) + .AddField(efb => efb.WithName("New Topic").WithValue(afterTextChannel.Topic)); + } + + await logChannel.EmbedAsync(embed).ConfigureAwait(false); } - catch (Exception ex) { _log.Warn(ex); } + catch { } } private static async void _client_ChannelDestroyed(IChannel ich) @@ -323,9 +421,13 @@ namespace NadekoBot.Modules.Administration if ((logChannel = await TryGetLogChannel(ch.Guild, logSetting, LogType.ChannelDestroyed)) == null) return; - await logChannel.SendMessageAsync($"🕕`{prettyCurrentTime}`🗑 **| {(ch is IVoiceChannel ? "Voice" : "Text")} Channel Deleted #⃣ {ch.Name}** `({ch.Id})`").ConfigureAwait(false); + await logChannel.EmbedAsync(new EmbedBuilder() + .WithOkColor() + .WithTitle("🆕 " + (ch is IVoiceChannel ? "Voice" : "Text") + " Channel Destroyed") + .WithDescription($"{ch.Name} | {ch.Id}") + .WithFooter(efb => efb.WithText(currentTime))).ConfigureAwait(false); } - catch (Exception ex) { _log.Warn(ex); } + catch { } } private static async void _client_ChannelCreated(IChannel ich) @@ -345,7 +447,11 @@ namespace NadekoBot.Modules.Administration if ((logChannel = await TryGetLogChannel(ch.Guild, logSetting, LogType.ChannelCreated)) == null) return; - await logChannel.SendMessageAsync($"🕓`{prettyCurrentTime}`🆕 **| {(ch is IVoiceChannel ? "Voice" : "Text")} Channel Created: #⃣ {ch.Name}** `({ch.Id})`").ConfigureAwait(false); + await logChannel.EmbedAsync(new EmbedBuilder() + .WithOkColor() + .WithTitle("🆕 " + (ch is IVoiceChannel ? "Voice" : "Text") + " Channel Created") + .WithDescription($"{ch.Name} | {ch.Id}") + .WithFooter(efb => efb.WithText(currentTime))).ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); } } @@ -387,19 +493,16 @@ namespace NadekoBot.Modules.Administration str = $"🎙`{prettyCurrentTime}`👤__**{usr.Username}#{usr.Discriminator}**__ has left **{beforeVch.Name}** voice channel."; } if (str != null) - UserPresenceUpdates.AddOrUpdate(logChannel, new List() { str }, (id, list) => { list.Add(str); return list; }); - } - catch (Exception ex) - { - _log.Warn(ex); + PresenceUpdates.AddOrUpdate(logChannel, new List() { str }, (id, list) => { list.Add(str); return list; }); } + catch { } } private static async void _client_UserPresenceUpdated(Optional optGuild, SocketUser usr, SocketPresence before, SocketPresence after) { try { - var guild = optGuild.IsSpecified ? optGuild.Value : null; + var guild = optGuild.GetValueOrDefault() ?? (usr as SocketGuildUser)?.Guild; if (guild == null) return; @@ -413,13 +516,18 @@ namespace NadekoBot.Modules.Administration ITextChannel logChannel; if ((logChannel = await TryGetLogChannel(guild, logSetting, LogType.UserPresence)) == null) return; - string str; + string str = ""; if (before.Status != after.Status) - str = $"🔵`{prettyCurrentTime}`👤__**{usr.Username}**__ is now **{after.Status}**."; - else - str = $"👾`{prettyCurrentTime}`👤__**{usr.Username}**__ is now playing **{after.Game}**."; + str = $"🎭`{prettyCurrentTime}`👤__**{usr.Username}**__ is now **{after.Status}**."; - UserPresenceUpdates.AddOrUpdate(logChannel, new List() { str }, (id, list) => { list.Add(str); return list; }); + //if (before.Game?.Name != after.Game?.Name) + //{ + // if (str != "") + // str += "\n"; + // str += $"👾`{prettyCurrentTime}`👤__**{usr.Username}**__ is now playing **{after.Game?.Name}**."; + //} + + PresenceUpdates.AddOrUpdate(logChannel, new List() { str }, (id, list) => { list.Add(str); return list; }); } catch { } } @@ -436,7 +544,14 @@ namespace NadekoBot.Modules.Administration ITextChannel logChannel; if ((logChannel = await TryGetLogChannel(usr.Guild, logSetting, LogType.UserLeft)) == null) return; - await logChannel.SendMessageAsync($"❗️🕛`{prettyCurrentTime}`👤__**{usr.Username}#{usr.Discriminator}**__❌ **| USER LEFT |** 🆔 `{usr.Id}`").ConfigureAwait(false); + + await logChannel.EmbedAsync(new EmbedBuilder() + .WithOkColor() + .WithTitle("❌ User Left") + .WithThumbnailUrl(usr.AvatarUrl) + .WithDescription(usr.ToString()) + .AddField(efb => efb.WithName("Id").WithValue(usr.Id.ToString())) + .WithFooter(efb => efb.WithText(currentTime))).ConfigureAwait(false); } catch { } } @@ -454,7 +569,13 @@ namespace NadekoBot.Modules.Administration if ((logChannel = await TryGetLogChannel(usr.Guild, logSetting, LogType.UserJoined)) == null) return; - await logChannel.SendMessageAsync($"❕🕓`{prettyCurrentTime}`👤__**{usr.Username}#{usr.Discriminator}**__✅ **| USER JOINED |** 🆔 `{usr.Id}`").ConfigureAwait(false); + await logChannel.EmbedAsync(new EmbedBuilder() + .WithOkColor() + .WithTitle("✅ User Joined") + .WithThumbnailUrl(usr.AvatarUrl) + .WithDescription($"{usr}") + .AddField(efb => efb.WithName("Id").WithValue(usr.Id.ToString())) + .WithFooter(efb => efb.WithText(currentTime))).ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); } } @@ -472,7 +593,13 @@ namespace NadekoBot.Modules.Administration if ((logChannel = await TryGetLogChannel(guild, logSetting, LogType.UserUnbanned)) == null) return; - await logChannel.SendMessageAsync($"❕🕘`{prettyCurrentTime}`👤__**{usr.Username}#{usr.Discriminator}**__♻️ **| USER UN-BANNED |** 🆔 `{usr.Id}`").ConfigureAwait(false); + await logChannel.EmbedAsync(new EmbedBuilder() + .WithOkColor() + .WithTitle("♻️ User Unbanned") + .WithThumbnailUrl(usr.AvatarUrl) + .WithDescription(usr.ToString()) + .AddField(efb => efb.WithName("Id").WithValue(usr.Id.ToString())) + .WithFooter(efb => efb.WithText(currentTime))).ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); } } @@ -489,7 +616,13 @@ namespace NadekoBot.Modules.Administration ITextChannel logChannel; if ((logChannel = await TryGetLogChannel(guild, logSetting, LogType.UserBanned)) == null) return; - await logChannel.SendMessageAsync($"‼️🕕`{prettyCurrentTime}`👤__**{usr.Username}#{usr.Discriminator}**__🚫 **| USER BANNED |** 🆔 `{usr.Id}`").ConfigureAwait(false); + await logChannel.EmbedAsync(new EmbedBuilder() + .WithOkColor() + .WithTitle("🚫 User Banned") + .WithThumbnailUrl(usr.AvatarUrl) + .WithDescription(usr.ToString()) + .AddField(efb => efb.WithName("Id").WithValue(usr.Id.ToString())) + .WithFooter(efb => efb.WithText(currentTime))).ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); } } @@ -516,13 +649,19 @@ namespace NadekoBot.Modules.Administration ITextChannel logChannel; if ((logChannel = await TryGetLogChannel(channel.Guild, logSetting, LogType.MessageDeleted)) == null || logChannel.Id == msg.Id) return; - var str = $@"🕔`{prettyCurrentTime}`👤__**{msg.Author.Username}#{msg.Author.Discriminator}**__ **| Deleted Message |** 🆔 `{msg.Author.Id}` #⃣ `{channel.Name}` -🗑 {msg.Resolve(userHandling: TagHandling.FullName)}"; + var embed = new EmbedBuilder() + .WithOkColor() + .WithTitle($"🗑 Message Deleted in {((ITextChannel)msg.Channel).Mention}") + .WithDescription($"{msg.Author}") + .AddField(efb => efb.WithName("Content").WithValue(msg.Resolve(userHandling: TagHandling.FullName)).WithIsInline(false)) + .AddField(efb => efb.WithName("Id").WithValue(msg.Id.ToString()).WithIsInline(false)) + .WithFooter(efb => efb.WithText(currentTime)); if (msg.Attachments.Any()) - str += $"{Environment.NewLine}📎 {string.Join(", ", msg.Attachments.Select(a => a.ProxyUrl))}"; - await logChannel.SendMessageAsync(str.SanitizeMentions()).ConfigureAwait(false); + embed.AddField(efb => efb.WithName("Attachments").WithValue(string.Join(", ", msg.Attachments.Select(a => a.ProxyUrl))).WithIsInline(false)); + + await logChannel.EmbedAsync(embed).ConfigureAwait(false); } - catch (Exception ex) { _log.Warn(ex); } + catch { } } private static async void _client_MessageUpdated(Optional optmsg, SocketMessage imsg2) @@ -553,11 +692,19 @@ namespace NadekoBot.Modules.Administration ITextChannel logChannel; if ((logChannel = await TryGetLogChannel(channel.Guild, logSetting, LogType.MessageUpdated)) == null || logChannel.Id == after.Channel.Id) return; - await logChannel.SendMessageAsync($@"🕔`{prettyCurrentTime}`👤__**{before.Author.Username}#{before.Author.Discriminator}**__ **| 📝 Edited Message |** 🆔 `{before.Author.Id}` #⃣ `{channel.Name}` - `Old:` {before.Resolve(userHandling: TagHandling.FullName).SanitizeMentions()} - **`New:`** {after.Resolve(userHandling: TagHandling.FullName).SanitizeMentions()}").ConfigureAwait(false); + + var embed = new EmbedBuilder() + .WithOkColor() + .WithTitle($"📝 Message Updated in {((ITextChannel)after.Channel).Mention}") + .WithDescription(after.Author.ToString()) + .AddField(efb => efb.WithName("Old Message").WithValue(before.Resolve(userHandling: TagHandling.FullName)).WithIsInline(false)) + .AddField(efb => efb.WithName("New Message").WithValue(after.Resolve(userHandling: TagHandling.FullName)).WithIsInline(false)) + .AddField(efb => efb.WithName("Id").WithValue(after.Id.ToString()).WithIsInline(false)) + .WithFooter(efb => efb.WithText(currentTime)); + + await logChannel.EmbedAsync(embed).ConfigureAwait(false); } - catch (Exception ex) { _log.Warn(ex); } + catch { } } public enum LogType @@ -740,14 +887,15 @@ namespace NadekoBot.Modules.Administration logSetting.ChannelUpdatedId = logSetting.LogUserPresenceId = logSetting.LogVoicePresenceId = + logSetting.UserMutedId = logSetting.LogVoicePresenceTTSId = (action.Value ? channel.Id : (ulong?)null); await uow.CompleteAsync().ConfigureAwait(false); } if (action.Value) - await channel.SendMessageAsync("✅ Logging all events on this channel.").ConfigureAwait(false); + await channel.SendConfirmAsync("Logging all events in this channel.").ConfigureAwait(false); else - await channel.SendMessageAsync("ℹ️ Logging disabled.").ConfigureAwait(false); + await channel.SendConfirmAsync("Logging disabled.").ConfigureAwait(false); } [NadekoCommand, Usage, Description, Aliases] @@ -774,9 +922,9 @@ namespace NadekoBot.Modules.Administration } if (removed == 0) - await channel.SendMessageAsync($"🆗 Logging will **now ignore** #⃣ `{channel.Name} ({channel.Id})`").ConfigureAwait(false); + await channel.SendConfirmAsync($"Logging will IGNORE **{channel.Mention} ({channel.Id})**").ConfigureAwait(false); else - await channel.SendMessageAsync($"ℹ️ Logging will **no longer ignore** #⃣ `{channel.Name} ({channel.Id})`").ConfigureAwait(false); + await channel.SendConfirmAsync($"Logging will NOT IGNORE **{channel.Mention} ({channel.Id})**").ConfigureAwait(false); } [NadekoCommand, Usage, Description, Aliases] @@ -853,9 +1001,9 @@ namespace NadekoBot.Modules.Administration } if (channelId != null) - await channel.SendMessageAsync($"✅ Logging `{type}` event in #⃣ `{channel.Name} ({channel.Id})`").ConfigureAwait(false); + await channel.SendConfirmAsync($"Logging **{type}** event in this channel.").ConfigureAwait(false); else - await channel.SendMessageAsync($"ℹ️ Stopped logging `{type}` event.").ConfigureAwait(false); + await channel.SendConfirmAsync($"Stopped logging **{type}** event.").ConfigureAwait(false); } } } diff --git a/src/NadekoBot/Modules/Administration/Commands/MessageRepeater.cs b/src/NadekoBot/Modules/Administration/Commands/MessageRepeater.cs deleted file mode 100644 index 8d50a80e..00000000 --- a/src/NadekoBot/Modules/Administration/Commands/MessageRepeater.cs +++ /dev/null @@ -1,173 +0,0 @@ -using Discord; -using Discord.Commands; -using NadekoBot.Attributes; -using NadekoBot.Extensions; -using NadekoBot.Services; -using NadekoBot.Services.Database.Models; -using NLog; -using System; -using System.Collections.Concurrent; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace NadekoBot.Modules.Administration -{ - public partial class Administration - { - [Group] - public class RepeatCommands : ModuleBase - { - public static ConcurrentDictionary repeaters { get; } - - public class RepeatRunner - { - private Logger _log { get; } - - private CancellationTokenSource source { get; set; } - private CancellationToken token { get; set; } - public Repeater Repeater { get; } - public ITextChannel Channel { get; } - - public RepeatRunner(Repeater repeater, ITextChannel channel = null) - { - _log = LogManager.GetCurrentClassLogger(); - this.Repeater = repeater; - this.Channel = channel ?? NadekoBot.Client.GetGuild(repeater.GuildId)?.GetTextChannelAsync(repeater.ChannelId).GetAwaiter().GetResult(); - if (Channel == null) - return; - Task.Run(Run); - } - - - private async Task Run() - { - source = new CancellationTokenSource(); - token = source.Token; - IUserMessage oldMsg = null; - try - { - while (!token.IsCancellationRequested) - { - var toSend = "🔄 " + Repeater.Message; - await Task.Delay(Repeater.Interval, token).ConfigureAwait(false); - - //var lastMsgInChannel = (await Channel.GetMessagesAsync(2)).FirstOrDefault(); - // if (lastMsgInChannel.Id == oldMsg?.Id) //don't send if it's the same message in the channel - // continue; - - if (oldMsg != null) - try { await oldMsg.DeleteAsync(); } catch { } - try { oldMsg = await Channel.SendMessageAsync(toSend).ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); } - } - } - catch (OperationCanceledException) { } - } - - public void Reset() - { - source.Cancel(); - var t = Task.Run(Run); - } - - public void Stop() - { - source.Cancel(); - } - } - - static RepeatCommands() - { - var _log = LogManager.GetCurrentClassLogger(); - var sw = Stopwatch.StartNew(); - using (var uow = DbHandler.UnitOfWork()) - { - repeaters = new ConcurrentDictionary(uow.Repeaters.GetAll().Select(r => new RepeatRunner(r)).Where(r => r != null).ToDictionary(r => r.Repeater.ChannelId)); - } - - sw.Stop(); - _log.Debug($"Loaded in {sw.Elapsed.TotalSeconds:F2}s"); - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - [RequireUserPermission(GuildPermission.ManageMessages)] - public async Task RepeatInvoke() - { - RepeatRunner rep; - if (!repeaters.TryGetValue(Context.Channel.Id, out rep)) - { - await Context.Channel.SendErrorAsync("ℹ️ **No repeating message found on this server.**").ConfigureAwait(false); - return; - } - rep.Reset(); - await Context.Channel.SendMessageAsync("🔄 " + rep.Repeater.Message).ConfigureAwait(false); - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - [RequireUserPermission(GuildPermission.ManageMessages)] - public async Task Repeat() - { - RepeatRunner rep; - if (repeaters.TryRemove(Context.Channel.Id, out rep)) - { - using (var uow = DbHandler.UnitOfWork()) - { - uow.Repeaters.Remove(rep.Repeater); - await uow.CompleteAsync(); - } - rep.Stop(); - await Context.Channel.SendConfirmAsync("✅ **Stopped repeating a message.**").ConfigureAwait(false); - } - else - await Context.Channel.SendConfirmAsync("ℹ️ **No message is repeating.**").ConfigureAwait(false); - } - - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - [RequireUserPermission(GuildPermission.ManageMessages)] - public async Task Repeat(int minutes, [Remainder] string message) - { - if (minutes < 1 || minutes > 10080) - return; - - if (string.IsNullOrWhiteSpace(message)) - return; - - RepeatRunner rep; - - rep = repeaters.AddOrUpdate(Context.Channel.Id, (cid) => - { - using (var uow = DbHandler.UnitOfWork()) - { - var localRep = new Repeater - { - ChannelId = Context.Channel.Id, - GuildId = Context.Guild.Id, - Interval = TimeSpan.FromMinutes(minutes), - Message = message, - }; - uow.Repeaters.Add(localRep); - uow.Complete(); - return new RepeatRunner(localRep, (ITextChannel)Context.Channel); - } - }, (cid, old) => - { - using (var uow = DbHandler.UnitOfWork()) - { - old.Repeater.Message = message; - old.Repeater.Interval = TimeSpan.FromMinutes(minutes); - uow.Repeaters.Update(old.Repeater); - uow.Complete(); - } - old.Reset(); - return old; - }); - - await Context.Channel.SendConfirmAsync($"🔁 Repeating **\"{rep.Repeater.Message}\"** every `{rep.Repeater.Interval.Days} day(s), {rep.Repeater.Interval.Hours} hour(s) and {rep.Repeater.Interval.Minutes} minute(s)`.").ConfigureAwait(false); - } - } - } -} diff --git a/src/NadekoBot/Modules/Administration/Commands/PlayingRotateCommands.cs b/src/NadekoBot/Modules/Administration/Commands/PlayingRotateCommands.cs index c9c49f60..b2e8115f 100644 --- a/src/NadekoBot/Modules/Administration/Commands/PlayingRotateCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/PlayingRotateCommands.cs @@ -71,8 +71,8 @@ namespace NadekoBot.Modules.Administration public static Dictionary> PlayingPlaceholders { get; } = new Dictionary> { - {"%servers%", () => NadekoBot.Client.GetGuilds().Count().ToString()}, - {"%users%", () => NadekoBot.Client.GetGuilds().Select(s => s.Users.Count).Sum().ToString()}, + {"%servers%", () => NadekoBot.Client.GetGuildsCount().ToString()}, + {"%users%", () => NadekoBot.Client.GetGuilds().Sum(s => s.Users.Count).ToString()}, {"%playing%", () => { var cnt = Music.Music.MusicPlayers.Count(kvp => kvp.Value.CurrentSong != null); if (cnt != 1) return cnt.ToString(); diff --git a/src/NadekoBot/Modules/Administration/Commands/ProtectionCommands.cs b/src/NadekoBot/Modules/Administration/Commands/ProtectionCommands.cs new file mode 100644 index 00000000..e37f4850 --- /dev/null +++ b/src/NadekoBot/Modules/Administration/Commands/ProtectionCommands.cs @@ -0,0 +1,425 @@ +using Discord; +using Discord.Commands; +using Microsoft.EntityFrameworkCore; +using NadekoBot.Attributes; +using NadekoBot.Extensions; +using NadekoBot.Services; +using NadekoBot.Services.Database; +using NadekoBot.Services.Database.Models; +using NLog; +using System; +using System.Collections.Concurrent; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Administration +{ + public partial class Administration + { + public enum ProtectionType + { + Raiding, + Spamming, + } + + public class AntiRaidStats + { + public AntiRaidSetting AntiRaidSettings { get; set; } + public int UsersCount { get; set; } = 0; + public ConcurrentHashSet RaidUsers { get; set; } = new ConcurrentHashSet(); + + public override string ToString() => + $"If **{AntiRaidSettings.UserThreshold}** or more users join within **{AntiRaidSettings.Seconds}** seconds," + + $" I will **{AntiRaidSettings.Action}** them."; + } + + public class AntiSpamStats + { + public AntiSpamSetting AntiSpamSettings { get; set; } + public ConcurrentDictionary UserStats { get; set; } + = new ConcurrentDictionary(); + + public override string ToString() + { + var ignoredString = string.Join(", ", AntiSpamSettings.IgnoredChannels.Select(c => $"<#{c.ChannelId}>")); + + if (string.IsNullOrWhiteSpace(ignoredString)) + ignoredString = "none"; + return $"If a user posts **{AntiSpamSettings.MessageThreshold}** same messages in a row, I will **{AntiSpamSettings.Action}** them." + + $"\n\t__IgnoredChannels__: {ignoredString}"; + } + } + + public class UserSpamStats + { + public int Count { get; set; } + public string LastMessage { get; set; } + + public UserSpamStats(string msg) + { + Count = 1; + LastMessage = msg.ToUpperInvariant(); + } + + public void ApplyNextMessage(string message) + { + var upperMsg = message.ToUpperInvariant(); + if (upperMsg == LastMessage) + Count++; + else + { + LastMessage = upperMsg; + Count = 0; + } + } + } + + [Group] + public class ProtectionCommands : ModuleBase + { + private static ConcurrentDictionary antiRaidGuilds = + new ConcurrentDictionary(); + // guildId | (userId|messages) + private static ConcurrentDictionary antiSpamGuilds = + new ConcurrentDictionary(); + + private static Logger _log { get; } + + static ProtectionCommands() + { + _log = LogManager.GetCurrentClassLogger(); + + foreach (var gc in NadekoBot.AllGuildConfigs) + { + var raid = gc.AntiRaidSetting; + var spam = gc.AntiSpamSetting; + + if (raid != null) + { + var raidStats = new AntiRaidStats() { AntiRaidSettings = raid }; + antiRaidGuilds.TryAdd(gc.GuildId, raidStats); + } + + if (spam != null) + antiSpamGuilds.TryAdd(gc.GuildId, new AntiSpamStats() { AntiSpamSettings = spam }); + } + + NadekoBot.Client.MessageReceived += async (imsg) => + { + + try + { + var msg = imsg as IUserMessage; + if (msg == null || msg.Author.IsBot) + return; + + var channel = msg.Channel as ITextChannel; + if (channel == null) + return; + AntiSpamStats spamSettings; + if (!antiSpamGuilds.TryGetValue(channel.Guild.Id, out spamSettings) || + spamSettings.AntiSpamSettings.IgnoredChannels.Contains(new AntiSpamIgnore() + { + ChannelId = channel.Id + })) + return; + + var stats = spamSettings.UserStats.AddOrUpdate(msg.Author.Id, new UserSpamStats(msg.Content), + (id, old) => { old.ApplyNextMessage(msg.Content); return old; }); + + if (stats.Count >= spamSettings.AntiSpamSettings.MessageThreshold) + { + if (spamSettings.UserStats.TryRemove(msg.Author.Id, out stats)) + { + await PunishUsers(spamSettings.AntiSpamSettings.Action, ProtectionType.Spamming, (IGuildUser)msg.Author) + .ConfigureAwait(false); + } + } + } + catch { } + }; + + NadekoBot.Client.UserJoined += async (usr) => + { + try + { + if (usr.IsBot) + return; + AntiRaidStats settings; + if (!antiRaidGuilds.TryGetValue(usr.Guild.Id, out settings)) + return; + if (!settings.RaidUsers.Add(usr)) + return; + + ++settings.UsersCount; + + if (settings.UsersCount >= settings.AntiRaidSettings.UserThreshold) + { + var users = settings.RaidUsers.ToArray(); + settings.RaidUsers.Clear(); + + await PunishUsers(settings.AntiRaidSettings.Action, ProtectionType.Raiding, users).ConfigureAwait(false); + } + await Task.Delay(1000 * settings.AntiRaidSettings.Seconds).ConfigureAwait(false); + + settings.RaidUsers.TryRemove(usr); + --settings.UsersCount; + + } + catch { } + }; + } + + private static async Task PunishUsers(PunishmentAction action, ProtectionType pt, params IGuildUser[] gus) + { + foreach (var gu in gus) + { + switch (action) + { + case PunishmentAction.Mute: + try + { + await MuteCommands.MuteUser(gu).ConfigureAwait(false); + } + catch (Exception ex) { _log.Warn(ex, "I can't apply punishement"); } + break; + case PunishmentAction.Kick: + try + { + await gu.Guild.AddBanAsync(gu, 7).ConfigureAwait(false); + try + { + await gu.Guild.RemoveBanAsync(gu).ConfigureAwait(false); + } + catch + { + await gu.Guild.RemoveBanAsync(gu).ConfigureAwait(false); + // try it twice, really don't want to ban user if + // only kick has been specified as the punishement + } + } + catch (Exception ex) { _log.Warn(ex, "I can't apply punishment"); } + break; + case PunishmentAction.Ban: + try + { + await gu.Guild.AddBanAsync(gu, 7).ConfigureAwait(false); + } + catch (Exception ex) { _log.Warn(ex, "I can't apply punishment"); } + break; + default: + break; + } + } + await LogCommands.TriggeredAntiProtection(gus, action, pt).ConfigureAwait(false); + } + + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + [RequireUserPermission(GuildPermission.Administrator)] + public async Task AntiRaid(int userThreshold = 5, int seconds = 10, PunishmentAction action = PunishmentAction.Mute) + { + if (userThreshold < 2 || userThreshold > 30) + { + await Context.Channel.SendErrorAsync("❗️User threshold must be between **2** and **30**.").ConfigureAwait(false); + return; + } + + if (seconds < 2 || seconds > 300) + { + await Context.Channel.SendErrorAsync("❗️Time must be between **2** and **300** seconds.").ConfigureAwait(false); + return; + } + + AntiRaidStats throwaway; + if (antiRaidGuilds.TryRemove(Context.Guild.Id, out throwaway)) + { + using (var uow = DbHandler.UnitOfWork()) + { + var gc = uow.GuildConfigs.For(Context.Guild.Id, set => set.Include(x => x.AntiRaidSetting)); + + gc.AntiRaidSetting = null; + await uow.CompleteAsync().ConfigureAwait(false); + } + await Context.Channel.SendConfirmAsync("**Anti-Raid** feature has been **disabled** on this server.").ConfigureAwait(false); + return; + } + + try + { + await MuteCommands.GetMuteRole(Context.Guild).ConfigureAwait(false); + } + catch (Exception ex) + { + await Context.Channel.SendConfirmAsync("⚠️ Failed creating a mute role. Give me ManageRoles permission" + + "or create 'nadeko-mute' role with disabled SendMessages and try again.") + .ConfigureAwait(false); + _log.Warn(ex); + return; + } + + var stats = new AntiRaidStats() + { + AntiRaidSettings = new AntiRaidSetting() + { + Action = action, + Seconds = seconds, + UserThreshold = userThreshold, + } + }; + + antiRaidGuilds.AddOrUpdate(Context.Guild.Id, stats, (key, old) => stats); + + using (var uow = DbHandler.UnitOfWork()) + { + var gc = uow.GuildConfigs.For(Context.Guild.Id, set => set.Include(x => x.AntiRaidSetting)); + + gc.AntiRaidSetting = stats.AntiRaidSettings; + await uow.CompleteAsync().ConfigureAwait(false); + } + + await Context.Channel.SendConfirmAsync("Anti-Raid Enabled", $"{Context.User.Mention} {stats.ToString()}") + .ConfigureAwait(false); + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + [RequireUserPermission(GuildPermission.Administrator)] + public async Task AntiSpam(int messageCount = 3, PunishmentAction action = PunishmentAction.Mute) + { + if (messageCount < 2 || messageCount > 10) + return; + + AntiSpamStats throwaway; + if (antiSpamGuilds.TryRemove(Context.Guild.Id, out throwaway)) + { + using (var uow = DbHandler.UnitOfWork()) + { + var gc = uow.GuildConfigs.For(Context.Guild.Id, set => set.Include(x => x.AntiSpamSetting) + .ThenInclude(x => x.IgnoredChannels)); + + gc.AntiSpamSetting = null; + await uow.CompleteAsync().ConfigureAwait(false); + } + await Context.Channel.SendConfirmAsync("**Anti-Spam** has been **disabled** on this server.").ConfigureAwait(false); + return; + } + + try + { + await MuteCommands.GetMuteRole(Context.Guild).ConfigureAwait(false); + } + catch (Exception ex) + { + await Context.Channel.SendErrorAsync("⚠️ Failed creating a mute role. Give me ManageRoles permission" + + "or create 'nadeko-mute' role with disabled SendMessages and try again.") + .ConfigureAwait(false); + _log.Warn(ex); + return; + } + + var stats = new AntiSpamStats + { + AntiSpamSettings = new AntiSpamSetting() + { + Action = action, + MessageThreshold = messageCount, + } + }; + + antiSpamGuilds.AddOrUpdate(Context.Guild.Id, stats, (key, old) => stats); + + using (var uow = DbHandler.UnitOfWork()) + { + var gc = uow.GuildConfigs.For(Context.Guild.Id, set => set.Include(x => x.AntiSpamSetting)); + + gc.AntiSpamSetting = stats.AntiSpamSettings; + await uow.CompleteAsync().ConfigureAwait(false); + } + + await Context.Channel.SendConfirmAsync("Anti-Spam Enabled", $"{Context.User.Mention} {stats.ToString()}").ConfigureAwait(false); + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task AntispamIgnore() + { + var channel = (ITextChannel)Context.Channel; + + var obj = new AntiSpamIgnore() + { + ChannelId = channel.Id + }; + bool added; + using (var uow = DbHandler.UnitOfWork()) + { + var gc = uow.GuildConfigs.For(Context.Guild.Id, set => set.Include(x => x.AntiSpamSetting).ThenInclude(x => x.IgnoredChannels)); + var spam = gc.AntiSpamSetting; + if (spam == null) + { + return; + } + + if (spam.IgnoredChannels.Add(obj)) + { + AntiSpamStats temp; + if (antiSpamGuilds.TryGetValue(Context.Guild.Id, out temp)) + temp.AntiSpamSettings.IgnoredChannels.Add(obj); + added = true; + } + else + { + spam.IgnoredChannels.Remove(obj); + AntiSpamStats temp; + if (antiSpamGuilds.TryGetValue(Context.Guild.Id, out temp)) + temp.AntiSpamSettings.IgnoredChannels.Remove(obj); + added = false; + } + + await uow.CompleteAsync().ConfigureAwait(false); + } + if (added) + await Context.Channel.SendConfirmAsync("Anti-Spam will ignore this channel.").ConfigureAwait(false); + else + await Context.Channel.SendConfirmAsync("Anti-Spam will no longer ignore this channel.").ConfigureAwait(false); + + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task AntiList() + { + var channel = (ITextChannel)Context.Channel; + + AntiSpamStats spam; + antiSpamGuilds.TryGetValue(Context.Guild.Id, out spam); + + AntiRaidStats raid; + antiRaidGuilds.TryGetValue(Context.Guild.Id, out raid); + + if (spam == null && raid == null) + { + await Context.Channel.SendConfirmAsync("No protections enabled."); + return; + } + + var embed = new EmbedBuilder().WithOkColor() + .WithTitle("Protections Enabled"); + + if (spam != null) + embed.AddField(efb => efb.WithName("Anti-Spam") + .WithValue(spam.ToString()) + .WithIsInline(true)); + + if (raid != null) + embed.AddField(efb => efb.WithName("Anti-Raid") + .WithValue(raid.ToString()) + .WithIsInline(true)); + + await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); + } + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs b/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs index 589fa49a..753f85f5 100644 --- a/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/SelfCommands.cs @@ -20,7 +20,7 @@ namespace NadekoBot.Modules.Administration public async Task Leave([Remainder] string guildStr) { guildStr = guildStr.Trim().ToUpperInvariant(); - var server = NadekoBot.Client.GetGuilds().FirstOrDefault(g => g.Id.ToString().Trim().ToUpperInvariant() == guildStr) ?? + var server = NadekoBot.Client.GetGuilds().FirstOrDefault(g => g.Id.ToString() == guildStr) ?? NadekoBot.Client.GetGuilds().FirstOrDefault(g => g.Name.Trim().ToUpperInvariant() == guildStr); if (server == null) @@ -59,7 +59,16 @@ namespace NadekoBot.Modules.Administration await NadekoBot.Client.CurrentUser().ModifyAsync(u => u.Username = newName).ConfigureAwait(false); - await Context.Channel.SendConfirmAsync($"ℹ️ Successfully changed name to **{newName}**").ConfigureAwait(false); + await Context.Channel.SendConfirmAsync($"Bot name changed to **{newName}**").ConfigureAwait(false); + } + + [NadekoCommand, Usage, Description, Aliases] + [OwnerOnly] + public async Task SetStatus([Remainder] SettableUserStatus status) + { + await NadekoBot.Client.SetStatus(status); + + await Context.Channel.SendConfirmAsync($"Bot status changed to **{status}**").ConfigureAwait(false); } [NadekoCommand, Usage, Description, Aliases] @@ -88,8 +97,6 @@ namespace NadekoBot.Modules.Administration [OwnerOnly] public async Task SetGame([Remainder] string game = null) { - game = game ?? ""; - await NadekoBot.Client.SetGame(game).ConfigureAwait(false); await Context.Channel.SendConfirmAsync("👾 **New game set.**").ConfigureAwait(false); diff --git a/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs b/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs index d69c31ca..8d95e5d4 100644 --- a/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs +++ b/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs @@ -145,13 +145,17 @@ namespace NadekoBot.Modules.CustomReactions if (customReactions == null || !customReactions.Any()) await Context.Channel.SendErrorAsync("No custom reactions found").ConfigureAwait(false); else - await Context.Channel.SendConfirmAsync( - $"Page {page} of custom reactions:", - string.Join("\n", customReactions.OrderBy(cr => cr.Trigger) - .Skip((page - 1) * 20) + { + var lastPage = customReactions.Count / 20; + await Context.Channel.SendPaginatedConfirmAsync(page, curPage => + new EmbedBuilder().WithOkColor() + .WithTitle("Custom reactions") + .WithDescription(string.Join("\n", customReactions.OrderBy(cr => cr.Trigger) + .Skip((curPage - 1) * 20) .Take(20) - .Select(cr => $"`#{cr.Id}` `Trigger:` {cr.Trigger}"))) + .Select(cr => $"`#{cr.Id}` `Trigger:` {cr.Trigger}"))), lastPage) .ConfigureAwait(false); + } } public enum All @@ -200,14 +204,22 @@ namespace NadekoBot.Modules.CustomReactions if (customReactions == null || !customReactions.Any()) await Context.Channel.SendErrorAsync("No custom reactions found").ConfigureAwait(false); else - await Context.Channel.SendConfirmAsync($"Page {page} of custom reactions (grouped):", - string.Join("\r\n", customReactions - .GroupBy(cr => cr.Trigger) - .OrderBy(cr => cr.Key) - .Skip((page - 1) * 20) - .Take(20) - .Select(cr => $"**{cr.Key.Trim().ToLowerInvariant()}** `x{cr.Count()}`"))) + { + var ordered = customReactions + .GroupBy(cr => cr.Trigger) + .OrderBy(cr => cr.Key) + .ToList(); + + var lastPage = ordered.Count / 20; + await Context.Channel.SendPaginatedConfirmAsync(page, (curPage) => + new EmbedBuilder().WithOkColor() + .WithTitle($"Custom Reactions (grouped)") + .WithDescription(string.Join("\r\n", ordered + .Skip((curPage - 1) * 20) + .Take(20) + .Select(cr => $"**{cr.Key.Trim().ToLowerInvariant()}** `x{cr.Count()}`"))), lastPage) .ConfigureAwait(false); + } } [NadekoCommand, Usage, Description, Aliases] @@ -300,13 +312,14 @@ namespace NadekoBot.Modules.CustomReactions { if (page < 1) return; - await Context.Channel.EmbedAsync(ReactionStats.OrderByDescending(x => x.Value) - .Skip((page - 1) * 9) - .Take(9) - .Aggregate(new EmbedBuilder().WithOkColor().WithTitle($"Custom Reaction stats page #{page}"), - (agg, cur) => agg.AddField(efb => efb.WithName(cur.Key).WithValue(cur.Value.ToString()).WithIsInline(true))) - ) - .ConfigureAwait(false); + var ordered = ReactionStats.OrderByDescending(x => x.Value).ToList(); + var lastPage = ordered.Count / 9; + await Context.Channel.SendPaginatedConfirmAsync(page, + (curPage) => ordered.Skip((curPage - 1) * 9) + .Take(9) + .Aggregate(new EmbedBuilder().WithOkColor().WithTitle($"Custom Reaction Stats"), + (agg, cur) => agg.AddField(efb => efb.WithName(cur.Key).WithValue(cur.Value.ToString()).WithIsInline(true))), lastPage) + .ConfigureAwait(false); } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Commands/DiceRollCommand.cs b/src/NadekoBot/Modules/Gambling/Commands/DiceRollCommand.cs index 90c54e2d..01283c83 100644 --- a/src/NadekoBot/Modules/Gambling/Commands/DiceRollCommand.cs +++ b/src/NadekoBot/Modules/Gambling/Commands/DiceRollCommand.cs @@ -164,12 +164,15 @@ namespace NadekoBot.Modules.Gambling var arr = new int[n1]; for (int i = 0; i < n1; i++) { - arr[i] = rng.Next(1, n2 + 1) + add - sub; + arr[i] = rng.Next(1, n2 + 1); } - var embed = new EmbedBuilder().WithOkColor().WithDescription($"{Context.User.Mention} rolled {n1} {(n1 == 1 ? "die" : "dice")} `1 to {n2}` +`{add}` -`{sub}`") - .AddField(efb => efb.WithName(Format.Bold("Result")) - .WithValue(string.Join(" ", (ordered ? arr.OrderBy(x => x).AsEnumerable() : arr).Select(x => Format.Code(x.ToString()))))); + var sum = arr.Sum(); + var embed = new EmbedBuilder().WithOkColor().WithDescription($"{Context.User.Mention} rolled {n1} {(n1 == 1 ? "die" : "dice")} `1 to {n2}`") + .AddField(efb => efb.WithName(Format.Bold("Rolls")) + .WithValue(string.Join(" ", (ordered ? arr.OrderBy(x => x).AsEnumerable() : arr).Select(x => Format.Code(x.ToString()))))) + .AddField(efb => efb.WithName(Format.Bold("Sum")) + .WithValue(sum + " + " + add + " - " + sub + " = " + (sum + add - sub))); await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); } } diff --git a/src/NadekoBot/Modules/Gambling/Commands/FlipCoinCommand.cs b/src/NadekoBot/Modules/Gambling/Commands/FlipCoinCommand.cs index 3e8c01a5..389c7927 100644 --- a/src/NadekoBot/Modules/Gambling/Commands/FlipCoinCommand.cs +++ b/src/NadekoBot/Modules/Gambling/Commands/FlipCoinCommand.cs @@ -26,9 +26,9 @@ namespace NadekoBot.Modules.Gambling if (count == 1) { if (rng.Next(0, 2) == 1) - await Context.Channel.SendFileAsync(headsPath, $"{Context.User.Mention} flipped " + Format.Code("Heads") + ".").ConfigureAwait(false); + await Context.Channel.SendFileAsync(File.Open(headsPath, FileMode.OpenOrCreate), "heads.jpg", $"{Context.User.Mention} flipped " + Format.Code("Heads") + ".").ConfigureAwait(false); else - await Context.Channel.SendFileAsync(tailsPath, $"{Context.User.Mention} flipped " + Format.Code("Tails") + ".").ConfigureAwait(false); + await Context.Channel.SendFileAsync(File.Open(tailsPath, FileMode.OpenOrCreate), "tails.jpg", $"{Context.User.Mention} flipped " + Format.Code("Tails") + ".").ConfigureAwait(false); return; } if (count > 10 || count < 1) @@ -93,7 +93,7 @@ namespace NadekoBot.Modules.Gambling str = $"{Context.User.Mention}`Better luck next time.`"; } - await Context.Channel.SendFileAsync(imgPathToSend, str).ConfigureAwait(false); + await Context.Channel.SendFileAsync(File.Open(imgPathToSend, FileMode.OpenOrCreate), "coin.jpg", str).ConfigureAwait(false); } } } diff --git a/src/NadekoBot/Modules/Gambling/Gambling.cs b/src/NadekoBot/Modules/Gambling/Gambling.cs index b3bbe1bd..bbff2239 100644 --- a/src/NadekoBot/Modules/Gambling/Gambling.cs +++ b/src/NadekoBot/Modules/Gambling/Gambling.cs @@ -99,7 +99,7 @@ namespace NadekoBot.Modules.Gambling await CurrencyHandler.AddCurrencyAsync(usrId, $"Awarded by bot owner. ({Context.User.Username}/{Context.User.Id})", amount).ConfigureAwait(false); - await Context.Channel.SendConfirmAsync($"{Context.User.Mention} successfully awarded {amount} {(amount == 1 ? Gambling.CurrencyName : Gambling.CurrencyPluralName)} to <@{usrId}>!").ConfigureAwait(false); + await Context.Channel.SendConfirmAsync($"{Context.User.Mention} awarded {amount} {(amount == 1 ? Gambling.CurrencyName : Gambling.CurrencyPluralName)} to <@{usrId}>!").ConfigureAwait(false); } [NadekoCommand, Usage, Description, Aliases] diff --git a/src/NadekoBot/Modules/Games/Commands/CleverBotCommands.cs b/src/NadekoBot/Modules/Games/Commands/CleverBotCommands.cs index 9ec59147..8197035b 100644 --- a/src/NadekoBot/Modules/Games/Commands/CleverBotCommands.cs +++ b/src/NadekoBot/Modules/Games/Commands/CleverBotCommands.cs @@ -91,7 +91,7 @@ namespace NadekoBot.Modules.Games [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - [RequireUserPermission(ChannelPermission.ManageMessages)] + [RequireUserPermission(GuildPermission.ManageMessages)] public async Task Cleverbot() { var channel = (ITextChannel)Context.Channel; diff --git a/src/NadekoBot/Modules/Games/Commands/Hangman/HangmanGame.cs b/src/NadekoBot/Modules/Games/Commands/Hangman/HangmanGame.cs index d096b452..23b76e9b 100644 --- a/src/NadekoBot/Modules/Games/Commands/Hangman/HangmanGame.cs +++ b/src/NadekoBot/Modules/Games/Commands/Hangman/HangmanGame.cs @@ -12,37 +12,15 @@ using System.Threading.Tasks; namespace NadekoBot.Modules.Games.Commands.Hangman { - public class HangmanModel - { - public List All { get; set; } - public List Animals { get; set; } - public List Countries { get; set; } - public List Movies { get; set; } - public List Things { get; set; } - } - public class HangmanTermPool { - public enum HangmanTermType - { - All, - Animals, - Countries, - Movies, - Things - } - const string termsPath = "data/hangman.json"; - public static HangmanModel data { get; } + public static IReadOnlyDictionary data { get; } static HangmanTermPool() { try { - data = JsonConvert.DeserializeObject(File.ReadAllText(termsPath)); - data.All = data.Animals.Concat(data.Countries) - .Concat(data.Movies) - .Concat(data.Things) - .ToList(); + data = JsonConvert.DeserializeObject>(File.ReadAllText(termsPath)); } catch (Exception ex) { @@ -50,27 +28,31 @@ namespace NadekoBot.Modules.Games.Commands.Hangman } } - public static HangmanObject GetTerm(HangmanTermType type) + public static HangmanObject GetTerm(string type) { + if (string.IsNullOrWhiteSpace(type)) + throw new ArgumentNullException(nameof(type)); + + type = type.Trim(); + var rng = new NadekoRandom(); - switch (type) - { - case HangmanTermType.Animals: - return data.Animals[rng.Next(0, data.Animals.Count)]; - case HangmanTermType.Countries: - return data.Countries[rng.Next(0, data.Countries.Count)]; - case HangmanTermType.Movies: - return data.Movies[rng.Next(0, data.Movies.Count)]; - case HangmanTermType.Things: - return data.Things[rng.Next(0, data.Things.Count)]; - default: - return data.All[rng.Next(0, data.All.Count)]; + + if (type == "All") { + var keys = data.Keys.ToArray(); + type = keys[rng.Next(0, keys.Length)]; } + HangmanObject[] termTypes; + data.TryGetValue(type, out termTypes); + + if (termTypes.Length == 0) + return null; + + return termTypes[rng.Next(0, termTypes.Length)]; } } - public class HangmanGame + public class HangmanGame: IDisposable { private readonly Logger _log; @@ -95,20 +77,23 @@ namespace NadekoBot.Modules.Games.Commands.Hangman public bool GuessedAll => Guesses.IsSupersetOf(Term.Word.ToUpperInvariant() .Where(c => char.IsLetter(c) || char.IsDigit(c))); - public HangmanTermPool.HangmanTermType TermType { get; } + public string TermType { get; } public event Action OnEnded; - public HangmanGame(IMessageChannel channel, HangmanTermPool.HangmanTermType type) + public HangmanGame(IMessageChannel channel, string type) { _log = LogManager.GetCurrentClassLogger(); this.GameChannel = channel; - this.TermType = type; + this.TermType = type.ToTitleCase(); } public void Start() { this.Term = HangmanTermPool.GetTerm(TermType); + + if (this.Term == null) + throw new KeyNotFoundException("Can't find a term with that type. Use hangmanlist command."); // start listening for answers when game starts NadekoBot.Client.MessageReceived += PotentialGuess; } @@ -211,5 +196,11 @@ namespace NadekoBot.Modules.Games.Commands.Hangman {(Errors > 1 ? "/" : " ")} {(Errors > 2 ? "|" : " ")} {(Errors > 3 ? "\\" : " ")} | {(Errors > 4 ? "/" : " ")} {(Errors > 5 ? "\\" : " ")} | /-\"; + + public void Dispose() + { + NadekoBot.Client.MessageReceived -= PotentialGuess; + OnEnded = null; + } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Commands/HangmanCommands.cs b/src/NadekoBot/Modules/Games/Commands/HangmanCommands.cs index eba11193..50596e4c 100644 --- a/src/NadekoBot/Modules/Games/Commands/HangmanCommands.cs +++ b/src/NadekoBot/Modules/Games/Commands/HangmanCommands.cs @@ -23,7 +23,7 @@ namespace NadekoBot.Modules.Games static HangmanCommands() { _log = LogManager.GetCurrentClassLogger(); - typesStr = $"`List of \"{NadekoBot.ModulePrefixes[typeof(Games).Name]}hangman\" term types:`\n" + String.Join(", ", Enum.GetNames(typeof(HangmanTermPool.HangmanTermType))); + typesStr = $"`List of \"{NadekoBot.ModulePrefixes[typeof(Games).Name]}hangman\" term types:`\n" + String.Join(", ", HangmanTermPool.data.Keys); } [NadekoCommand, Usage, Description, Aliases] @@ -33,7 +33,7 @@ namespace NadekoBot.Modules.Games } [NadekoCommand, Usage, Description, Aliases] - public async Task Hangman(HangmanTermPool.HangmanTermType type = HangmanTermPool.HangmanTermType.All) + public async Task Hangman([Remainder]string type = "All") { var hm = new HangmanGame(Context.Channel, type); @@ -48,7 +48,18 @@ namespace NadekoBot.Modules.Games HangmanGame throwaway; HangmanGames.TryRemove(g.GameChannel.Id, out throwaway); }; - hm.Start(); + try + { + hm.Start(); + } + catch (Exception ex) + { + try { await Context.Channel.SendErrorAsync($"Starting errored: {ex.Message}").ConfigureAwait(false); } catch { } + HangmanGame throwaway; + HangmanGames.TryRemove(Context.Channel.Id, out throwaway); + throwaway.Dispose(); + return; + } await Context.Channel.SendConfirmAsync("Hangman game started", hm.ScrambledWord + "\n" + hm.GetHangman() + "\n" + hm.ScrambledWord); } diff --git a/src/NadekoBot/Modules/Games/Commands/PlantAndPickCommands.cs b/src/NadekoBot/Modules/Games/Commands/PlantAndPickCommands.cs index 560f9f45..015b4328 100644 --- a/src/NadekoBot/Modules/Games/Commands/PlantAndPickCommands.cs +++ b/src/NadekoBot/Modules/Games/Commands/PlantAndPickCommands.cs @@ -46,9 +46,12 @@ namespace NadekoBot.Modules.Games { _log = LogManager.GetCurrentClassLogger(); var sw = Stopwatch.StartNew(); + + #if !GLOBAL_NADEKO NadekoBot.Client.MessageReceived += PotentialFlowerGeneration; #endif + using (var uow = DbHandler.UnitOfWork()) { var conf = uow.BotConfig.GetOrCreate(); @@ -91,7 +94,8 @@ namespace NadekoBot.Modules.Games lastGenerations.AddOrUpdate(channel.Id, DateTime.Now, (id, old) => DateTime.Now); var sent = await channel.SendFileAsync( - GetRandomCurrencyImagePath(), + File.Open(GetRandomCurrencyImagePath(), FileMode.OpenOrCreate), + "RandomFlower.jpg", $"❗ A random { Gambling.Gambling.CurrencyName } appeared! Pick it up by typing `{NadekoBot.ModulePrefixes[typeof(Games).Name]}pick`") .ConfigureAwait(false); plantedFlowers.AddOrUpdate(channel.Id, new List() { sent }, (id, old) => { old.Add(sent); return old; }); @@ -101,16 +105,19 @@ namespace NadekoBot.Modules.Games } catch { } } -#if !GLOBAL_NADEKO + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task Pick() { var channel = (ITextChannel)Context.Channel; - if (!(await channel.Guild.GetCurrentUserAsync()).GetPermissions(channel).ManageMessages || !usersRecentlyPicked.Add(Context.User.Id)) + if (!(await channel.Guild.GetCurrentUserAsync()).GetPermissions(channel).ManageMessages) return; - +#if GLOBAL_NADEKO + if (!usersRecentlyPicked.Add(Context.User.Id)) + return; +#endif try { @@ -122,14 +129,16 @@ namespace NadekoBot.Modules.Games await Task.WhenAll(msgs.Select(toDelete => toDelete.DeleteAsync())).ConfigureAwait(false); - await CurrencyHandler.AddCurrencyAsync((IGuildUser)Context.User, "Picked flower(s).", msgs.Count, false).ConfigureAwait(false); + await CurrencyHandler.AddCurrencyAsync((IGuildUser)Context.User, $"Picked {Gambling.Gambling.CurrencyPluralName}", msgs.Count, false).ConfigureAwait(false); var msg = await channel.SendConfirmAsync($"**{Context.User}** picked {msgs.Count}{Gambling.Gambling.CurrencySign}!").ConfigureAwait(false); msg.DeleteAfter(10); } finally { +#if GLOBAL_NADEKO await Task.Delay(60000); usersRecentlyPicked.TryRemove(Context.User.Id); +#endif } } @@ -137,7 +146,7 @@ namespace NadekoBot.Modules.Games [RequireContext(ContextType.Guild)] public async Task Plant() { - var removed = await CurrencyHandler.RemoveCurrencyAsync((IGuildUser)Context.User, "Planted a flower.", 1, false).ConfigureAwait(false); + var removed = await CurrencyHandler.RemoveCurrencyAsync((IGuildUser)Context.User, $"Planted a {Gambling.Gambling.CurrencyName}", 1, false).ConfigureAwait(false); if (!removed) { await Context.Channel.SendErrorAsync($"You don't have any {Gambling.Gambling.CurrencyPluralName}.").ConfigureAwait(false); @@ -155,11 +164,11 @@ namespace NadekoBot.Modules.Games } else { - msg = await Context.Channel.SendFileAsync(file, msgToSend).ConfigureAwait(false); + msg = await Context.Channel.SendFileAsync(File.Open(file, FileMode.OpenOrCreate), "plant.jpg", msgToSend).ConfigureAwait(false); } plantedFlowers.AddOrUpdate(Context.Channel.Id, new List() { msg }, (id, old) => { old.Add(msg); return old; }); } -#endif + [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [RequireUserPermission(GuildPermission.ManageMessages)] diff --git a/src/NadekoBot/Modules/Games/Commands/PollCommands.cs b/src/NadekoBot/Modules/Games/Commands/PollCommands.cs index f7321a72..01d0477c 100644 --- a/src/NadekoBot/Modules/Games/Commands/PollCommands.cs +++ b/src/NadekoBot/Modules/Games/Commands/PollCommands.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; @@ -17,7 +18,7 @@ namespace NadekoBot.Modules.Games [Group] public class PollCommands : ModuleBase { - public static ConcurrentDictionary ActivePolls = new ConcurrentDictionary(); + public static ConcurrentDictionary ActivePolls = new ConcurrentDictionary(); [NadekoCommand, Usage, Description, Aliases] [RequireUserPermission(GuildPermission.ManageMessages)] @@ -31,6 +32,18 @@ namespace NadekoBot.Modules.Games public Task PublicPoll([Remainder] string arg = null) => InternalStartPoll(arg, isPublic: true); + [NadekoCommand, Usage, Description, Aliases] + [RequireUserPermission(GuildPermission.ManageMessages)] + [RequireContext(ContextType.Guild)] + public async Task PollStats() + { + Games.Poll poll; + if (!ActivePolls.TryGetValue(Context.Guild.Id, out poll)) + return; + + await Context.Channel.EmbedAsync(poll.GetStats("Current Poll Results")); + } + private async Task InternalStartPoll(string arg, bool isPublic = false) { var channel = (ITextChannel)Context.Channel; @@ -44,7 +57,7 @@ namespace NadekoBot.Modules.Games return; var poll = new Poll(Context.Message, data[0], data.Skip(1), isPublic: isPublic); - if (ActivePolls.TryAdd(channel.Guild, poll)) + if (ActivePolls.TryAdd(channel.Guild.Id, poll)) { await poll.StartPoll().ConfigureAwait(false); } @@ -60,7 +73,7 @@ namespace NadekoBot.Modules.Games var channel = (ITextChannel)Context.Channel; Poll poll; - ActivePolls.TryRemove(channel.Guild, out poll); + ActivePolls.TryRemove(channel.Guild.Id, out poll); await poll.StopPoll().ConfigureAwait(false); } } @@ -69,20 +82,55 @@ namespace NadekoBot.Modules.Games { private readonly IUserMessage originalMessage; private readonly IGuild guild; - private readonly string[] answers; + private string[] Answers { get; } private ConcurrentDictionary participants = new ConcurrentDictionary(); private readonly string question; private DateTime started; private CancellationTokenSource pollCancellationSource = new CancellationTokenSource(); - private readonly bool isPublic; + public bool IsPublic { get; } public Poll(IUserMessage umsg, string question, IEnumerable enumerable, bool isPublic = false) { this.originalMessage = umsg; this.guild = ((ITextChannel)umsg.Channel).Guild; this.question = question; - this.answers = enumerable as string[] ?? enumerable.ToArray(); - this.isPublic = isPublic; + this.Answers = enumerable as string[] ?? enumerable.ToArray(); + this.IsPublic = isPublic; + } + + public EmbedBuilder GetStats(string title) + { + var results = participants.GroupBy(kvp => kvp.Value) + .ToDictionary(x => x.Key, x => x.Sum(kvp => 1)) + .OrderByDescending(kvp => kvp.Value) + .ToArray(); + + var eb = new EmbedBuilder().WithTitle(title); + + var sb = new StringBuilder() + .AppendLine(Format.Bold(question)) + .AppendLine(); + + var totalVotesCast = 0; + if (results.Length == 0) + { + sb.AppendLine("No votes cast."); + } + else + { + for (int i = 0; i < results.Length; i++) + { + var result = results[i]; + sb.AppendLine($"`{i + 1}.` {Format.Bold(Answers[result.Key - 1])} with {Format.Bold(result.Value.ToString())} votes."); + totalVotesCast += result.Value; + } + } + + + eb.WithDescription(sb.ToString()) + .WithFooter(efb => efb.WithText(totalVotesCast + " total votes cast.")); + + return eb; } public async Task StartPoll() @@ -91,8 +139,8 @@ namespace NadekoBot.Modules.Games NadekoBot.Client.MessageReceived += Vote; var msgToSend = $"📃**{originalMessage.Author.Username}** has created a poll which requires your attention:\n\n**{question}**\n"; var num = 1; - msgToSend = answers.Aggregate(msgToSend, (current, answ) => current + $"`{num++}.` **{answ}**\n"); - if (!isPublic) + msgToSend = Answers.Aggregate(msgToSend, (current, answ) => current + $"`{num++}.` **{answ}**\n"); + if (!IsPublic) msgToSend += "\n**Private Message me with the corresponding number of the answer.**"; else msgToSend += "\n**Send a Message here with the corresponding number of the answer.**"; @@ -102,30 +150,7 @@ namespace NadekoBot.Modules.Games public async Task StopPoll() { NadekoBot.Client.MessageReceived -= Vote; - try - { - var results = participants.GroupBy(kvp => kvp.Value) - .ToDictionary(x => x.Key, x => x.Sum(kvp => 1)) - .OrderByDescending(kvp => kvp.Value); - - var totalVotesCast = results.Sum(kvp => kvp.Value); - if (totalVotesCast == 0) - { - await originalMessage.Channel.SendMessageAsync("📄 **No votes have been cast.**").ConfigureAwait(false); - return; - } - var closeMessage = $"--------------**POLL CLOSED**--------------\n" + - $"📄 , here are the results:\n"; - closeMessage = results.Aggregate(closeMessage, (current, kvp) => current + $"`{kvp.Key}.` **[{answers[kvp.Key - 1]}]**" + - $" has {kvp.Value} votes." + - $"({kvp.Value * 1.0f / totalVotesCast * 100}%)\n"); - - await originalMessage.Channel.SendConfirmAsync($"📄 **Total votes cast**: {totalVotesCast}\n{closeMessage}").ConfigureAwait(false); - } - catch (Exception ex) - { - Console.WriteLine($"Error in poll game {ex}"); - } + await originalMessage.Channel.EmbedAsync(GetStats("POLL CLOSED")).ConfigureAwait(false); } private async void Vote(SocketMessage imsg) @@ -141,11 +166,11 @@ namespace NadekoBot.Modules.Games int vote; if (!int.TryParse(imsg.Content, out vote)) return; - if (vote < 1 || vote > answers.Length) + if (vote < 1 || vote > Answers.Length) return; IMessageChannel ch; - if (isPublic) + if (IsPublic) { //if public, channel must be the same the poll started in if (originalMessage.Channel.Id != imsg.Channel.Id) @@ -167,7 +192,7 @@ namespace NadekoBot.Modules.Games //user can vote only once if (participants.TryAdd(msg.Author.Id, vote)) { - if (!isPublic) + if (!IsPublic) { await ch.SendConfirmAsync($"Thanks for voting **{msg.Author.Username}**.").ConfigureAwait(false); } diff --git a/src/NadekoBot/Modules/Help/Help.cs b/src/NadekoBot/Modules/Help/Help.cs index 3d32eaca..87f31de0 100644 --- a/src/NadekoBot/Modules/Help/Help.cs +++ b/src/NadekoBot/Modules/Help/Help.cs @@ -122,17 +122,23 @@ namespace NadekoBot.Modules.Help [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [OwnerOnly] - public Task Hgit() + public async Task Hgit() { var helpstr = new StringBuilder(); helpstr.AppendLine("You can support the project on patreon: or paypal: \n"); helpstr.AppendLine("##Table Of Contents"); - helpstr.AppendLine(string.Join("\n", NadekoBot.CommandService.Modules.Where(m => m.Name.ToLowerInvariant() != "help").OrderBy(m => m.Name).Prepend(NadekoBot.CommandService.Modules.FirstOrDefault(m=>m.Name.ToLowerInvariant()=="help")).Select(m => $"- [{m.Name}](#{m.Name.ToLowerInvariant()})"))); + helpstr.AppendLine(string.Join("\n", NadekoBot.CommandService.Modules.Where(m => m.GetTopLevelModule().Name.ToLowerInvariant() != "help") + .Select(m => m.GetTopLevelModule().Name) + .Distinct() + .OrderBy(m => m) + .Prepend("Help") + .Select(m => $"- [{m}](#{m.ToLowerInvariant()})"))); helpstr.AppendLine(); string lastModule = null; - foreach (var com in NadekoBot.CommandService.Commands.OrderBy(com => com.Module.Name).GroupBy(c => c.Aliases.First()).Select(g => g.First())) + foreach (var com in NadekoBot.CommandService.Commands.OrderBy(com => com.Module.GetTopLevelModule().Name).GroupBy(c => c.Aliases.First()).Select(g => g.First())) { - if (com.Module.Name != lastModule) + var module = com.Module.GetTopLevelModule(); + if (module.Name != lastModule) { if (lastModule != null) { @@ -140,16 +146,16 @@ namespace NadekoBot.Modules.Help helpstr.AppendLine("###### [Back to TOC](#table-of-contents)"); } helpstr.AppendLine(); - helpstr.AppendLine("### " + com.Module.Name + " "); + helpstr.AppendLine("### " + module.Name + " "); helpstr.AppendLine("Command and aliases | Description | Usage"); helpstr.AppendLine("----------------|--------------|-------"); - lastModule = com.Module.Name; + lastModule = module.Name; } helpstr.AppendLine($"{string.Join(" ", com.Aliases.Select(a => "`" + a + "`"))} | {string.Format(com.Summary, com.Module.GetPrefix())} {GetCommandRequirements(com)} | {string.Format(com.Remarks, com.Module.GetPrefix())}"); } helpstr = helpstr.Replace(NadekoBot.Client.CurrentUser().Username , "@BotName"); File.WriteAllText("../../docs/Commands List.md", helpstr.ToString()); - return Task.CompletedTask; + await Context.Channel.SendConfirmAsync("Commandlist Regenerated").ConfigureAwait(false); } [NadekoCommand, Usage, Description, Aliases] diff --git a/src/NadekoBot/Modules/Music/Classes/MusicControls.cs b/src/NadekoBot/Modules/Music/Classes/MusicControls.cs index 4b15153b..1e822f0a 100644 --- a/src/NadekoBot/Modules/Music/Classes/MusicControls.cs +++ b/src/NadekoBot/Modules/Music/Classes/MusicControls.cs @@ -77,7 +77,7 @@ namespace NadekoBot.Modules.Music.Classes public string PrettyVolume => $"🔉 {(int)(Volume * 100)}%"; - public event Action SongRemoved = delegate { }; + public event Action SongRemoved = delegate { }; public MusicPlayer(IVoiceChannel startingVoiceChannel, float? defaultVolume) { @@ -137,7 +137,7 @@ namespace NadekoBot.Modules.Music.Classes var index = playlist.IndexOf(CurrentSong); if (index != -1) - RemoveSongAt(index); + RemoveSongAt(index, true); OnStarted(this, CurrentSong); await CurrentSong.Play(audioClient, cancelToken); @@ -273,16 +273,16 @@ namespace NadekoBot.Modules.Music.Classes }); } - public void RemoveSongAt(int index) + public void RemoveSongAt(int index, bool silent = false) { actionQueue.Enqueue(() => { if (index < 0 || index >= playlist.Count) return; var song = playlist.ElementAtOrDefault(index); - if (playlist.Remove(song)) + if (playlist.Remove(song) && !silent) { - SongRemoved(song); + SongRemoved(song, index); } }); diff --git a/src/NadekoBot/Modules/Music/Classes/Song.cs b/src/NadekoBot/Modules/Music/Classes/Song.cs index 6a614eec..0ee23fd4 100644 --- a/src/NadekoBot/Modules/Music/Classes/Song.cs +++ b/src/NadekoBot/Modules/Music/Classes/Song.cs @@ -62,7 +62,15 @@ namespace NadekoBot.Modules.Music.Classes else if (TotalTime == TimeSpan.MaxValue) return "∞"; else - return TotalTime.ToString(@"mm\:ss"); + { + var time = TotalTime.ToString(@"mm\:ss"); + var hrs = (int)TotalTime.TotalHours; + + if (hrs > 0) + return hrs + ":" + time; + else + return time; + } } } diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 7ecef7c5..381177a2 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -99,16 +99,16 @@ namespace NadekoBot.Modules.Music [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - public async Task Destroy() + public Task Destroy() { - await Context.Channel.SendErrorAsync("This command is temporarily disabled.").ConfigureAwait(false); - - /*MusicPlayer musicPlayer; - if (!MusicPlayers.TryGetValue(channel.Guild.Id, out musicPlayer)) return Task.CompletedTask; - if (((IGuildUser)umsg.Author).VoiceChannel == musicPlayer.PlaybackVoiceChannel) - if(MusicPlayers.TryRemove(channel.Guild.Id, out musicPlayer)) + MusicPlayer musicPlayer; + if (!MusicPlayers.TryGetValue(Context.Guild.Id, out musicPlayer)) return Task.CompletedTask; + if (((IGuildUser)Context.User).VoiceChannel == musicPlayer.PlaybackVoiceChannel) + if (MusicPlayers.TryRemove(Context.Guild.Id, out musicPlayer)) musicPlayer.Destroy(); - return Task.CompletedTask;*/ + + return Task.CompletedTask; + } [NadekoCommand, Usage, Description, Aliases] @@ -183,36 +183,41 @@ namespace NadekoBot.Modules.Music try { await musicPlayer.UpdateSongDurationsAsync().ConfigureAwait(false); } catch { } const int itemsPerPage = 10; - int startAt = itemsPerPage * (page - 1); - var number = 0 + startAt; var total = musicPlayer.TotalPlaytime; var maxPlaytime = musicPlayer.MaxPlaytimeSeconds; - var embed = new EmbedBuilder() - .WithAuthor(eab => eab.WithName($"Player Queue - Page {page}") - .WithMusicIcon()) - .WithDescription(string.Join("\n", musicPlayer.Playlist - .Skip(startAt) - .Take(10) - .Select(v => $"`{++number}.` {v.PrettyFullName}"))) - .WithFooter(ef => ef.WithText($"{musicPlayer.PrettyVolume} | {musicPlayer.Playlist.Count} " + -$"{("tracks".SnPl(musicPlayer.Playlist.Count))} | {(int)total.TotalHours}h {total.Minutes}m {total.Seconds}s | " + -(musicPlayer.FairPlay ? "✔️fairplay" : "✖️fairplay") + $" | " + (maxPlaytime == 0 ? "unlimited" : $"{maxPlaytime}s limit"))) - .WithOkColor(); + var lastPage = musicPlayer.Playlist.Count / itemsPerPage; + Func printAction = (curPage) => + { + int startAt = itemsPerPage * (curPage - 1); + var number = 0 + startAt; + var embed = new EmbedBuilder() + .WithAuthor(eab => eab.WithName($"Player Queue") + .WithMusicIcon()) + .WithDescription(string.Join("\n", musicPlayer.Playlist + .Skip(startAt) + .Take(itemsPerPage) + .Select(v => $"`{++number}.` {v.PrettyFullName}"))) + .WithFooter(ef => ef.WithText($"{musicPlayer.PrettyVolume} | {musicPlayer.Playlist.Count} " + + $"{("tracks".SnPl(musicPlayer.Playlist.Count))} | {(int)total.TotalHours}h {total.Minutes}m {total.Seconds}s | " + + (musicPlayer.FairPlay ? "✔️fairplay" : "✖️fairplay") + $" | " + (maxPlaytime == 0 ? "unlimited" : $"{maxPlaytime}s limit"))) + .WithOkColor(); - if (musicPlayer.RepeatSong) - { - embed.WithTitle($"🔂 Repeating Song: {currentSong.SongInfo.Title} | {currentSong.PrettyFullTime}"); - } - else if (musicPlayer.RepeatPlaylist) - { - embed.WithTitle("🔁 Repeating Playlist"); - } - if (musicPlayer.MaxQueueSize != 0 && musicPlayer.Playlist.Count >= musicPlayer.MaxQueueSize) - { - embed.WithTitle("🎵 Song queue is full!"); - } - await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); + if (musicPlayer.RepeatSong) + { + embed.WithTitle($"🔂 Repeating Song: {currentSong.SongInfo.Title} | {currentSong.PrettyFullTime}"); + } + else if (musicPlayer.RepeatPlaylist) + { + embed.WithTitle("🔁 Repeating Playlist"); + } + if (musicPlayer.MaxQueueSize != 0 && musicPlayer.Playlist.Count >= musicPlayer.MaxQueueSize) + { + embed.WithTitle("🎵 Song queue is full!"); + } + return embed; + }; + await Context.Channel.SendPaginatedConfirmAsync(page, printAction, lastPage).ConfigureAwait(false); } [NadekoCommand, Usage, Description, Aliases] @@ -300,7 +305,7 @@ $"{("tracks".SnPl(musicPlayer.Playlist.Count))} | {(int)total.TotalHours}h {tota return; if (((IGuildUser)Context.User).VoiceChannel?.Guild != Context.Guild) { - await Context.Channel.SendErrorAsync("💢 You need to be in a **voice channel** on this server.\n If you are already in a voice (ITextChannel)Context.Channel, try rejoining it.").ConfigureAwait(false); + await Context.Channel.SendErrorAsync($"💢 You need to be in a **voice channel** on this server.").ConfigureAwait(false); return; } var plId = (await NadekoBot.Google.GetPlaylistIdsByKeywordsAsync(arg).ConfigureAwait(false)).FirstOrDefault(); @@ -458,31 +463,16 @@ $"{("tracks".SnPl(musicPlayer.Playlist.Count))} | {(int)total.TotalHours}h {tota [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [Priority(0)] - public async Task Remove(int num) + public Task Remove(int num) { MusicPlayer musicPlayer; if (!MusicPlayers.TryGetValue(Context.Guild.Id, out musicPlayer)) - return; + return Task.CompletedTask; if (((IGuildUser)Context.User).VoiceChannel != musicPlayer.PlaybackVoiceChannel) - return; - - musicPlayer.SongRemoved += async (song) => - { - try - { - var embed = new EmbedBuilder() - .WithAuthor(eab => eab.WithName("Removed song #" + num).WithMusicIcon()) - .WithDescription(song.PrettyName) - .WithFooter(ef => ef.WithText(song.PrettyInfo)) - .WithErrorColor(); - - await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); - - } - catch { } - }; + return Task.CompletedTask; musicPlayer.RemoveSongAt(num - 1); + return Task.CompletedTask; } [NadekoCommand, Usage, Description, Aliases] @@ -490,8 +480,6 @@ $"{("tracks".SnPl(musicPlayer.Playlist.Count))} | {(int)total.TotalHours}h {tota [Priority(1)] public async Task Remove(string all) { - - if (all.Trim().ToUpperInvariant() != "ALL") return; MusicPlayer musicPlayer; @@ -857,13 +845,34 @@ $"{("tracks".SnPl(musicPlayer.Playlist.Count))} | {(int)total.TotalHours}h {tota { try { + IUserMessage msg; if (paused) - await textCh.SendConfirmAsync("🎵 Music playback **paused**.").ConfigureAwait(false); + msg = await textCh.SendConfirmAsync("🎵 Music playback **paused**.").ConfigureAwait(false); else - await textCh.SendConfirmAsync("🎵 Music playback **resumed**.").ConfigureAwait(false); + msg = await textCh.SendConfirmAsync("🎵 Music playback **resumed**.").ConfigureAwait(false); + + if (msg != null) + msg.DeleteAfter(10); } catch { } }; + + + mp.SongRemoved += async (song, index) => + { + try + { + var embed = new EmbedBuilder() + .WithAuthor(eab => eab.WithName("Removed song #" + (index + 1)).WithMusicIcon()) + .WithDescription(song.PrettyName) + .WithFooter(ef => ef.WithText(song.PrettyInfo)) + .WithErrorColor(); + + await textCh.EmbedAsync(embed).ConfigureAwait(false); + + } + catch { } + }; return mp; }); Song resolvedSong; diff --git a/src/NadekoBot/Modules/NSFW/NSFW.cs b/src/NadekoBot/Modules/NSFW/NSFW.cs index 7c692f0b..4db4c1b3 100644 --- a/src/NadekoBot/Modules/NSFW/NSFW.cs +++ b/src/NadekoBot/Modules/NSFW/NSFW.cs @@ -49,7 +49,7 @@ namespace NadekoBot.Modules.NSFW var link = await provider.ConfigureAwait(false); if (string.IsNullOrWhiteSpace(link)) { - if (noError) + if (!noError) await channel.SendErrorAsync("No results found.").ConfigureAwait(false); return; } diff --git a/src/NadekoBot/Modules/Permissions/Commands/BlacklistCommands.cs b/src/NadekoBot/Modules/Permissions/Commands/BlacklistCommands.cs index 72c41bc3..e02dc895 100644 --- a/src/NadekoBot/Modules/Permissions/Commands/BlacklistCommands.cs +++ b/src/NadekoBot/Modules/Permissions/Commands/BlacklistCommands.cs @@ -23,13 +23,18 @@ namespace NadekoBot.Modules.Permissions [Group] public class BlacklistCommands : ModuleBase { - public static ConcurrentHashSet BlacklistedItems { get; set; } = new ConcurrentHashSet(); + public static ConcurrentHashSet BlacklistedUsers { get; set; } = new ConcurrentHashSet(); + public static ConcurrentHashSet BlacklistedGuilds { get; set; } = new ConcurrentHashSet(); + public static ConcurrentHashSet BlacklistedChannels { get; set; } = new ConcurrentHashSet(); static BlacklistCommands() { using (var uow = DbHandler.UnitOfWork()) { - BlacklistedItems = new ConcurrentHashSet(uow.BotConfig.GetOrCreate().Blacklist); + var blacklist = uow.BotConfig.GetOrCreate().Blacklist; + BlacklistedUsers = new ConcurrentHashSet(blacklist.Where(bi => bi.Type == BlacklistType.User).Select(c => c.ItemId)); + BlacklistedGuilds = new ConcurrentHashSet(blacklist.Where(bi => bi.Type == BlacklistType.Server).Select(c => c.ItemId)); + BlacklistedChannels = new ConcurrentHashSet(blacklist.Where(bi => bi.Type == BlacklistType.Channel).Select(c => c.ItemId)); } } @@ -66,12 +71,34 @@ namespace NadekoBot.Modules.Permissions { var item = new BlacklistItem { ItemId = id, Type = type }; uow.BotConfig.GetOrCreate().Blacklist.Add(item); - BlacklistedItems.Add(item); + if (type == BlacklistType.Server) + { + BlacklistedGuilds.Add(id); + } + else if (type == BlacklistType.Channel) + { + BlacklistedChannels.Add(id); + } + else if (type == BlacklistType.User) + { + BlacklistedUsers.Add(id); + } } else { uow.BotConfig.GetOrCreate().Blacklist.RemoveWhere(bi => bi.ItemId == id && bi.Type == type); - BlacklistedItems.RemoveWhere(bi => bi.ItemId == id && bi.Type == type); + if (type == BlacklistType.Server) + { + BlacklistedGuilds.TryRemove(id); + } + else if (type == BlacklistType.Channel) + { + BlacklistedChannels.TryRemove(id); + } + else if (type == BlacklistType.User) + { + BlacklistedUsers.TryRemove(id); + } } await uow.CompleteAsync().ConfigureAwait(false); } diff --git a/src/NadekoBot/Modules/Permissions/Permissions.cs b/src/NadekoBot/Modules/Permissions/Permissions.cs index d2b282aa..33d8a27e 100644 --- a/src/NadekoBot/Modules/Permissions/Permissions.cs +++ b/src/NadekoBot/Modules/Permissions/Permissions.cs @@ -73,6 +73,9 @@ namespace NadekoBot.Modules.Permissions [RequireContext(ContextType.Guild)] public async Task PermRole([Remainder] IRole role = null) { + if (role != null && role == role.Guild.EveryoneRole) + return; + using (var uow = DbHandler.UnitOfWork()) { var config = uow.GuildConfigs.For(Context.Guild.Id, set => set); @@ -379,6 +382,9 @@ namespace NadekoBot.Modules.Permissions [RequireContext(ContextType.Guild)] public async Task RoleCmd(CommandInfo command, PermissionAction action, [Remainder] IRole role) { + if (role == role.Guild.EveryoneRole) + return; + using (var uow = DbHandler.UnitOfWork()) { var newPerm = new Permission @@ -405,6 +411,9 @@ namespace NadekoBot.Modules.Permissions [RequireContext(ContextType.Guild)] public async Task RoleMdl(ModuleInfo module, PermissionAction action, [Remainder] IRole role) { + if (role == role.Guild.EveryoneRole) + return; + using (var uow = DbHandler.UnitOfWork()) { var newPerm = new Permission @@ -515,6 +524,9 @@ namespace NadekoBot.Modules.Permissions [RequireContext(ContextType.Guild)] public async Task AllRoleMdls(PermissionAction action, [Remainder] IRole role) { + if (role == role.Guild.EveryoneRole) + return; + using (var uow = DbHandler.UnitOfWork()) { var newPerm = new Permission diff --git a/src/NadekoBot/Modules/Searches/Commands/Models/WeatherModels.cs b/src/NadekoBot/Modules/Searches/Commands/Models/WeatherModels.cs index d3c5742f..371b2065 100644 --- a/src/NadekoBot/Modules/Searches/Commands/Models/WeatherModels.cs +++ b/src/NadekoBot/Modules/Searches/Commands/Models/WeatherModels.cs @@ -23,8 +23,8 @@ namespace NadekoBot.Modules.Searches.Commands.Models public class Main { public double temp { get; set; } - public int pressure { get; set; } - public int humidity { get; set; } + public float pressure { get; set; } + public float humidity { get; set; } public double temp_min { get; set; } public double temp_max { get; set; } } diff --git a/src/NadekoBot/Modules/Searches/Commands/OMDB/OmdbProvider.cs b/src/NadekoBot/Modules/Searches/Commands/OMDB/OmdbProvider.cs index 892f8fab..a3d56fcf 100644 --- a/src/NadekoBot/Modules/Searches/Commands/OMDB/OmdbProvider.cs +++ b/src/NadekoBot/Modules/Searches/Commands/OMDB/OmdbProvider.cs @@ -18,7 +18,8 @@ namespace NadekoBot.Modules.Searches.Commands.OMDB { var res = await http.GetStringAsync(String.Format(queryUrl,name.Trim().Replace(' ','+'))).ConfigureAwait(false); var movie = JsonConvert.DeserializeObject(res); - + if (movie?.Title == null) + return null; movie.Poster = await NadekoBot.Google.ShortenUrl(movie.Poster); return movie; } diff --git a/src/NadekoBot/Modules/Searches/Searches.cs b/src/NadekoBot/Modules/Searches/Searches.cs index b919335c..16b67e18 100644 --- a/src/NadekoBot/Modules/Searches/Searches.cs +++ b/src/NadekoBot/Modules/Searches/Searches.cs @@ -109,61 +109,75 @@ namespace NadekoBot.Modules.Searches } [NadekoCommand, Usage, Description, Aliases] - public async Task I([Remainder] string query = null) + public async Task Image([Remainder] string terms = null) { - if (string.IsNullOrWhiteSpace(query)) + terms = terms?.Trim(); + if (string.IsNullOrWhiteSpace(terms)) return; - try - { - using (var http = new HttpClient()) - { - var reqString = $"https://www.googleapis.com/customsearch/v1?q={Uri.EscapeDataString(query)}&cx=018084019232060951019%3Ahs5piey28-e&num=1&searchType=image&fields=items%2Flink&key={NadekoBot.Credentials.GoogleApiKey}"; - var obj = JObject.Parse(await http.GetStringAsync(reqString).ConfigureAwait(false)); - await Context.Channel.SendMessageAsync(obj["items"][0]["link"].ToString()).ConfigureAwait(false); - } - } - catch (HttpRequestException exception) - { - if (exception.Message.Contains("403 (Forbidden)")) - { - await Context.Channel.SendErrorAsync("Daily limit reached!"); - } - else - { - await Context.Channel.SendErrorAsync("Something went wrong."); - _log.Error(exception); - } - } + + terms = WebUtility.UrlEncode(terms).Replace(' ', '+'); + + var fullQueryLink = $"http://imgur.com/search?q={ terms }"; + var config = Configuration.Default.WithDefaultLoader(); + var document = await BrowsingContext.New(config).OpenAsync(fullQueryLink); + + var elems = document.QuerySelectorAll("a.image-list-link"); + + if (!elems.Any()) + return; + + var img = (elems.FirstOrDefault()?.Children?.FirstOrDefault() as IHtmlImageElement); + + if (img?.Source == null) + return; + + var source = img.Source.Replace("b.", "."); + + var embed = new EmbedBuilder() + .WithOkColor() + .WithAuthor(eab => eab.WithName("Image Search For: " + terms.TrimTo(50)) + .WithUrl(fullQueryLink) + .WithIconUrl("http://s.imgur.com/images/logo-1200-630.jpg?")) + .WithDescription(source) + .WithImageUrl(source) + .WithTitle(Context.User.Mention); + await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); } [NadekoCommand, Usage, Description, Aliases] - public async Task Ir([Remainder] string query = null) + public async Task RandomImage([Remainder] string terms = null) { - if (string.IsNullOrWhiteSpace(query)) + terms = terms?.Trim(); + if (string.IsNullOrWhiteSpace(terms)) return; - try - { - using (var http = new HttpClient()) - { - var rng = new NadekoRandom(); - var reqString = $"https://www.googleapis.com/customsearch/v1?q={Uri.EscapeDataString(query)}&cx=018084019232060951019%3Ahs5piey28-e&num=1&searchType=image&start={ rng.Next(1, 50) }&fields=items%2Flink&key={NadekoBot.Credentials.GoogleApiKey}"; - var obj = JObject.Parse(await http.GetStringAsync(reqString).ConfigureAwait(false)); - var items = obj["items"] as JArray; - await Context.Channel.SendMessageAsync(items[0]["link"].ToString()).ConfigureAwait(false); - } - } - catch (HttpRequestException exception) - { - if (exception.Message.Contains("403 (Forbidden)")) - { - await Context.Channel.SendErrorAsync("Daily limit reached!"); - } - else - { - await Context.Channel.SendErrorAsync("Something went wrong."); - _log.Error(exception); - } - } + + terms = WebUtility.UrlEncode(terms).Replace(' ', '+'); + + var fullQueryLink = $"http://imgur.com/search?q={ terms }"; + var config = Configuration.Default.WithDefaultLoader(); + var document = await BrowsingContext.New(config).OpenAsync(fullQueryLink); + + var elems = document.QuerySelectorAll("a.image-list-link").ToList(); + + if (!elems.Any()) + return; + + var img = (elems.ElementAtOrDefault(new NadekoRandom().Next(0, elems.Count))?.Children?.FirstOrDefault() as IHtmlImageElement); + + if (img?.Source == null) + return; + + var source = img.Source.Replace("b.", "."); + + var embed = new EmbedBuilder() + .WithOkColor() + .WithAuthor(eab => eab.WithName("Image Search For: " + terms.TrimTo(50)) + .WithUrl(fullQueryLink) + .WithIconUrl("http://s.imgur.com/images/logo-1200-630.jpg?")) + .WithDescription(source) + .WithImageUrl(source) + .WithTitle(Context.User.Mention); + await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); } [NadekoCommand, Usage, Description, Aliases] @@ -568,14 +582,10 @@ namespace NadekoBot.Modules.Searches public async Task Color([Remainder] string color = null) { color = color?.Trim().Replace("#", ""); - if (string.IsNullOrWhiteSpace((string)color)) + if (string.IsNullOrWhiteSpace(color)) return; var img = new ImageSharp.Image(50, 50); - var red = Convert.ToInt32(color.Substring(0, 2), 16); - var green = Convert.ToInt32(color.Substring(2, 2), 16); - var blue = Convert.ToInt32(color.Substring(4, 2), 16); - img.BackgroundColor(new ImageSharp.Color(color)); await Context.Channel.SendFileAsync(img.ToStream(), $"{color}.png"); diff --git a/src/NadekoBot/Modules/Administration/Commands/CrossServerTextChannel.cs b/src/NadekoBot/Modules/Utility/Commands/CrossServerTextChannel.cs similarity index 97% rename from src/NadekoBot/Modules/Administration/Commands/CrossServerTextChannel.cs rename to src/NadekoBot/Modules/Utility/Commands/CrossServerTextChannel.cs index 62af02da..b2ae710c 100644 --- a/src/NadekoBot/Modules/Administration/Commands/CrossServerTextChannel.cs +++ b/src/NadekoBot/Modules/Utility/Commands/CrossServerTextChannel.cs @@ -9,9 +9,9 @@ using System.Collections.Concurrent; using System.Linq; using System.Threading.Tasks; -namespace NadekoBot.Modules.Administration +namespace NadekoBot.Modules.Utility { - public partial class Administration + public partial class Utility { [Group] public class CrossServerTextChannel : ModuleBase diff --git a/src/NadekoBot/Modules/Utility/Commands/InfoCommands.cs b/src/NadekoBot/Modules/Utility/Commands/InfoCommands.cs index ca2ab738..8814c50c 100644 --- a/src/NadekoBot/Modules/Utility/Commands/InfoCommands.cs +++ b/src/NadekoBot/Modules/Utility/Commands/InfoCommands.cs @@ -44,7 +44,7 @@ namespace NadekoBot.Modules.Utility .AddField(fb => fb.WithName("**Voice Channels**").WithValue(voicechn.ToString()).WithIsInline(true)) .AddField(fb => fb.WithName("**Created At**").WithValue($"{createdAt.ToString("dd.MM.yyyy HH:mm")}").WithIsInline(true)) .AddField(fb => fb.WithName("**Region**").WithValue(guild.VoiceRegionId.ToString()).WithIsInline(true)) - .AddField(fb => fb.WithName("**Roles**").WithValue(guild.Roles.Count().ToString()).WithIsInline(true)) + .AddField(fb => fb.WithName("**Roles**").WithValue((guild.Roles.Count - 1).ToString()).WithIsInline(true)) .WithImageUrl(guild.IconUrl) .WithColor(NadekoBot.OkColor); if (guild.Emojis.Count() > 0) @@ -92,8 +92,7 @@ namespace NadekoBot.Modules.Utility embed.AddField(fb => fb.WithName("**ID**").WithValue(user.Id.ToString()).WithIsInline(true)) .AddField(fb => fb.WithName("**Joined Server**").WithValue($"{user.JoinedAt?.ToString("dd.MM.yyyy HH:mm")}").WithIsInline(true)) .AddField(fb => fb.WithName("**Joined Discord**").WithValue($"{user.CreatedAt.ToString("dd.MM.yyyy HH:mm")}").WithIsInline(true)) - .AddField(fb => fb.WithName("**Current Game**").WithValue($"{(user.Game?.Name == null ? "-" : user.Game.Value.Name)}").WithIsInline(true)) - .AddField(fb => fb.WithName("**Roles**").WithValue($"**({user.RoleIds.Count})** - {string.Join(", ", user.GetRoles().Select(r => r.Name)).SanitizeMentions()}").WithIsInline(true)) + .AddField(fb => fb.WithName("**Roles**").WithValue($"**({user.RoleIds.Count})** - {string.Join(", ", user.GetRoles().Where(r => r.Id != r.Guild.EveryoneRole.Id).Select(r => r.Name)).SanitizeMentions()}").WithIsInline(true)) .WithThumbnailUrl(user.AvatarUrl) .WithColor(NadekoBot.OkColor); await Context.Channel.EmbedAsync(embed).ConfigureAwait(false); @@ -103,7 +102,7 @@ namespace NadekoBot.Modules.Utility [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [OwnerOnly] - public async Task Activity(IUserMessage imsg, int page = 1) + public async Task Activity(int page = 1) { const int activityPerPage = 15; page -= 1; @@ -119,7 +118,7 @@ namespace NadekoBot.Modules.Utility str.AppendLine($"`{++startCount}.` **{kvp.Key}** [{kvp.Value/NadekoBot.Stats.GetUptime().TotalSeconds:F2}/s] - {kvp.Value} total"); } - await imsg.Channel.EmbedAsync(new EmbedBuilder().WithTitle($"Activity Page #{page}") + await Context.Channel.EmbedAsync(new EmbedBuilder().WithTitle($"Activity Page #{page}") .WithOkColor() .WithFooter(efb => efb.WithText($"{NadekoBot.CommandHandler.UserMessagesSent.Count} users total.")) .WithDescription(str.ToString())); diff --git a/src/NadekoBot/Modules/Utility/Commands/MessageRepeater.cs b/src/NadekoBot/Modules/Utility/Commands/MessageRepeater.cs new file mode 100644 index 00000000..71bd6748 --- /dev/null +++ b/src/NadekoBot/Modules/Utility/Commands/MessageRepeater.cs @@ -0,0 +1,238 @@ +using Discord; +using Discord.Commands; +using Microsoft.EntityFrameworkCore; +using NadekoBot.Attributes; +using NadekoBot.Extensions; +using NadekoBot.Services; +using NadekoBot.Services.Database; +using NadekoBot.Services.Database.Models; +using NLog; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Utility +{ + public partial class Utility + { + [Group] + public class RepeatCommands : ModuleBase + { + //guildid/RepeatRunners + public static ConcurrentDictionary> repeaters { get; } + + public class RepeatRunner + { + private Logger _log { get; } + + private CancellationTokenSource source { get; set; } + private CancellationToken token { get; set; } + public Repeater Repeater { get; } + public ITextChannel Channel { get; } + + public RepeatRunner(Repeater repeater, ITextChannel channel = null) + { + _log = LogManager.GetCurrentClassLogger(); + this.Repeater = repeater; + this.Channel = channel ?? NadekoBot.Client.GetGuild(repeater.GuildId)?.GetTextChannelAsync(repeater.ChannelId).GetAwaiter().GetResult(); + if (Channel == null) + return; + Task.Run(Run); + } + + + private async Task Run() + { + source = new CancellationTokenSource(); + token = source.Token; + IUserMessage oldMsg = null; + try + { + while (!token.IsCancellationRequested) + { + var toSend = "🔄 " + Repeater.Message; + await Task.Delay(Repeater.Interval, token).ConfigureAwait(false); + + //var lastMsgInChannel = (await Channel.GetMessagesAsync(2)).FirstOrDefault(); + // if (lastMsgInChannel.Id == oldMsg?.Id) //don't send if it's the same message in the channel + // continue; + + if (oldMsg != null) + try { await oldMsg.DeleteAsync(); } catch { } + try { oldMsg = await Channel.SendMessageAsync(toSend).ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); } + } + } + catch (OperationCanceledException) { } + } + + public void Reset() + { + source.Cancel(); + var t = Task.Run(Run); + } + + public void Stop() + { + source.Cancel(); + } + + public override string ToString() + { + return $"{this.Channel.Mention} | {(int)this.Repeater.Interval.TotalHours}:{this.Repeater.Interval:mm} | {this.Repeater.Message.TrimTo(33)}"; + } + } + + static RepeatCommands() + { + var _log = LogManager.GetCurrentClassLogger(); + var sw = Stopwatch.StartNew(); + + repeaters = new ConcurrentDictionary>(NadekoBot.AllGuildConfigs + .ToDictionary(gc => gc.GuildId, + gc => new ConcurrentQueue(gc.GuildRepeaters.Select(gr => new RepeatRunner(gr)) + .Where(gr => gr.Channel != null)))); + + sw.Stop(); + _log.Debug($"Loaded in {sw.Elapsed.TotalSeconds:F2}s"); + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + [RequireUserPermission(GuildPermission.ManageMessages)] + public async Task RepeatInvoke(int index) + { + index -= 1; + ConcurrentQueue rep; + if (!repeaters.TryGetValue(Context.Guild.Id, out rep)) + { + await Context.Channel.SendErrorAsync("ℹ️ **No repeating message found on this server.**").ConfigureAwait(false); + return; + } + + var repList = rep.ToList(); + + if (index >= repList.Count) + { + await Context.Channel.SendErrorAsync("Index out of range.").ConfigureAwait(false); + return; + } + var repeater = repList[index].Repeater; + + await Context.Channel.SendMessageAsync("🔄 " + repeater.Message).ConfigureAwait(false); + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + [RequireUserPermission(GuildPermission.ManageMessages)] + [Priority(0)] + public async Task RepeatRemove(int index) + { + if (index < 1) + return; + index -= 1; + + ConcurrentQueue rep; + if (!repeaters.TryGetValue(Context.Guild.Id, out rep)) + return; + + var repeaterList = rep.ToList(); + + if (index >= repeaterList.Count) + { + await Context.Channel.SendErrorAsync("Index out of range.").ConfigureAwait(false); + return; + } + + var repeater = repeaterList[index]; + repeater.Stop(); + repeaterList.RemoveAt(index); + + using (var uow = DbHandler.UnitOfWork()) + { + var guildConfig = uow.GuildConfigs.For(Context.Guild.Id, set => set.Include(gc => gc.GuildRepeaters)); + + guildConfig.GuildRepeaters.RemoveWhere(r => r.Id == repeater.Repeater.Id); + await uow.CompleteAsync().ConfigureAwait(false); + } + + if (repeaters.TryUpdate(Context.Guild.Id, new ConcurrentQueue(repeaterList), rep)) + await Context.Channel.SendConfirmAsync("Message Repeater",$"#{index+1} stopped.\n\n{repeater.ToString()}").ConfigureAwait(false); + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + [RequireUserPermission(GuildPermission.ManageMessages)] + [Priority(1)] + public async Task Repeat(int minutes, [Remainder] string message) + { + if (minutes < 1 || minutes > 10080) + return; + + if (string.IsNullOrWhiteSpace(message)) + return; + + var toAdd = new GuildRepeater() + { + ChannelId = Context.Channel.Id, + GuildId = Context.Guild.Id, + Interval = TimeSpan.FromMinutes(minutes), + Message = message + }; + + using (var uow = DbHandler.UnitOfWork()) + { + var gc = uow.GuildConfigs.For(Context.Guild.Id, set => set.Include(x => x.GuildRepeaters)); + + if (gc.GuildRepeaters.Count >= 5) + return; + gc.GuildRepeaters.Add(toAdd); + + await uow.CompleteAsync().ConfigureAwait(false); + } + + var rep = new RepeatRunner(toAdd, (ITextChannel)Context.Channel); + + repeaters.AddOrUpdate(Context.Guild.Id, new ConcurrentQueue(new[] { rep }), (key, old) => + { + old.Enqueue(rep); + return old; + }); + + await Context.Channel.SendConfirmAsync($"🔁 Repeating **\"{rep.Repeater.Message}\"** every `{rep.Repeater.Interval.Days} day(s), {rep.Repeater.Interval.Hours} hour(s) and {rep.Repeater.Interval.Minutes} minute(s)`.").ConfigureAwait(false); + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + [RequireUserPermission(GuildPermission.ManageMessages)] + public async Task RepeatList() + { + ConcurrentQueue repRunners; + if (!repeaters.TryGetValue(Context.Guild.Id, out repRunners)) + { + await Context.Channel.SendConfirmAsync("No repeaters running on this server.").ConfigureAwait(false); + return; + } + + var replist = repRunners.ToList(); + var sb = new StringBuilder(); + + for (int i = 0; i < replist.Count; i++) + { + var rep = replist[i]; + + sb.AppendLine($"`{i + 1}.` {rep.ToString()}"); + } + + await Context.Channel.EmbedAsync(new EmbedBuilder().WithOkColor() + .WithTitle("List Of Repeaters") + .WithDescription(sb.ToString())) + .ConfigureAwait(false); + } + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Utility/Commands/Remind.cs b/src/NadekoBot/Modules/Utility/Commands/Remind.cs index 6cb3965f..58660bce 100644 --- a/src/NadekoBot/Modules/Utility/Commands/Remind.cs +++ b/src/NadekoBot/Modules/Utility/Commands/Remind.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; namespace NadekoBot.Modules.Utility diff --git a/src/NadekoBot/Modules/Utility/Utility.cs b/src/NadekoBot/Modules/Utility/Utility.cs index 27f8d25e..4a581108 100644 --- a/src/NadekoBot/Modules/Utility/Utility.cs +++ b/src/NadekoBot/Modules/Utility/Utility.cs @@ -10,12 +10,84 @@ using System.Text.RegularExpressions; using System.Reflection; using NadekoBot.Services.Impl; using System.Net.Http; +using System.Collections.Concurrent; +using System.Threading; +using ImageSharp; +using System.Collections.Generic; +using Newtonsoft.Json; namespace NadekoBot.Modules.Utility { [NadekoModule("Utility", ".")] public partial class Utility : DiscordModule { + private static ConcurrentDictionary rotatingRoleColors = new ConcurrentDictionary(); + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + [OwnerOnly] + public async Task RotateRoleColor(int timeout, IRole role, params string[] hexes) + { + var channel = (ITextChannel)Context.Channel; + + if ((timeout < 60 && timeout != 0) || timeout > 3600) + return; + + Timer t; + if (timeout == 0 || hexes.Length == 0) + { + if (rotatingRoleColors.TryRemove(role.Id, out t)) + { + t.Change(Timeout.Infinite, Timeout.Infinite); + await channel.SendConfirmAsync($"Stopped rotating colors for the **{role.Name}** role").ConfigureAwait(false); + } + return; + } + + var hexColors = hexes.Select(hex => + { + try { return (ImageSharp.Color?)new ImageSharp.Color(hex.Replace("#", "")); } catch { return null; } + }) + .Where(c => c != null) + .Select(c => c.Value) + .ToArray(); + + if (!hexColors.Any()) + { + await channel.SendMessageAsync("No colors are in the correct format. Use `#00ff00` for example.").ConfigureAwait(false); + return; + } + + var images = hexColors.Select(color => + { + var img = new ImageSharp.Image(50, 50); + img.BackgroundColor(color); + return img; + }).Merge().ToStream(); + + var i = 0; + t = new Timer(async (_) => + { + try + { + var color = hexColors[i]; + await role.ModifyAsync(r => r.Color = new Discord.Color(color.R, color.G, color.B)).ConfigureAwait(false); + ++i; + if (i >= hexColors.Length) + i = 0; + } + catch { } + }, null, 0, timeout * 1000); + + rotatingRoleColors.AddOrUpdate(role.Id, t, (key, old) => + { + old.Change(Timeout.Infinite, Timeout.Infinite); + return t; + }); + + await channel.SendFileAsync(images, "magicalgirl.jpg", $"Rotating **{role.Name}** role's color.").ConfigureAwait(false); + } + [NadekoCommand, Usage, Description, Aliases] public async Task TogetherTube() { @@ -170,15 +242,27 @@ namespace NadekoBot.Modules.Utility [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - public async Task ChannelTopic() + public async Task ChannelTopic([Remainder]ITextChannel channel = null) { - var channel = (ITextChannel)Context.Channel; + if (channel == null) + channel = (ITextChannel)Context.Channel; var topic = channel.Topic; if (string.IsNullOrWhiteSpace(topic)) - await channel.SendErrorAsync("No topic set."); + await Context.Channel.SendErrorAsync("No topic set.").ConfigureAwait(false); else - await channel.SendConfirmAsync("Channel topic", topic); + await Context.Channel.SendConfirmAsync("Channel topic", topic).ConfigureAwait(false); + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + [RequireBotPermission(ChannelPermission.CreateInstantInvite)] + [RequireUserPermission(ChannelPermission.CreateInstantInvite)] + public async Task CreateInvite() + { + var invite = await ((ITextChannel)Context.Channel).CreateInviteAsync(0, null, isUnique: true); + + await Context.Channel.SendConfirmAsync($"{Context.User.Mention} https://discord.gg/{invite.Code}"); } [NadekoCommand, Usage, Description, Aliases] @@ -199,7 +283,7 @@ namespace NadekoBot.Modules.Utility .AddField(efb => efb.WithName(Format.Bold("Memory")).WithValue($"{stats.Heap} MB").WithIsInline(true)) .AddField(efb => efb.WithName(Format.Bold("Owner ID(s)")).WithValue(stats.OwnerIds).WithIsInline(true)) .AddField(efb => efb.WithName(Format.Bold("Uptime")).WithValue(stats.GetUptimeString("\n")).WithIsInline(true)) - .AddField(efb => efb.WithName(Format.Bold("Presence")).WithValue($"{NadekoBot.Client.GetGuilds().Count} Servers\n{stats.TextChannels} Text Channels\n{stats.VoiceChannels} Voice Channels").WithIsInline(true)) + .AddField(efb => efb.WithName(Format.Bold("Presence")).WithValue($"{NadekoBot.Client.GetGuildsCount()} Servers\n{stats.TextChannels} Text Channels\n{stats.VoiceChannels} Voice Channels").WithIsInline(true)) #if !GLOBAL_NADEKO .WithFooter(efb => efb.WithText($"Playing {Music.Music.MusicPlayers.Where(mp => mp.Value.CurrentSong != null).Count()} songs, {Music.Music.MusicPlayers.Sum(mp => mp.Value.Playlist.Count)} queued.")) #endif @@ -230,7 +314,7 @@ namespace NadekoBot.Modules.Utility if (page < 0) return; - var guilds = NadekoBot.Client.GetGuilds().OrderBy(g => g.Name).Skip((page - 1) * 15).Take(15); + var guilds = await Task.Run(() => NadekoBot.Client.GetGuilds().OrderBy(g => g.Name).Skip((page - 1) * 15).Take(15)).ConfigureAwait(false); if (!guilds.Any()) { @@ -244,5 +328,22 @@ namespace NadekoBot.Modules.Utility .WithIsInline(false)))) .ConfigureAwait(false); } + + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + [OwnerOnly] + public async Task SaveChat(int cnt) + { + var sb = new StringBuilder(); + var msgs = new List(cnt); + await Context.Channel.GetMessagesAsync(cnt).ForEachAsync(dled => msgs.AddRange(dled)).ConfigureAwait(false); + + var title = $"Chatlog-{Context.Guild.Name}/#{Context.Channel.Name}-{DateTime.Now}.txt"; + var grouping = msgs.GroupBy(x => $"{x.CreatedAt.Date:dd.MM.yyyy}") + .Select(g => new { date = g.Key, messages = g.OrderBy(x => x.CreatedAt).Select(s => $"【{s.Timestamp:HH:mm:ss}】{s.Author}:" + s.ToString()) }); + await Context.User.SendFileAsync( + await JsonConvert.SerializeObject(grouping, Formatting.Indented).ToStream().ConfigureAwait(false), title, title).ConfigureAwait(false); + } } } \ No newline at end of file diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index 8b7bba44..9f72a07f 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -91,7 +91,9 @@ namespace NadekoBot //connect await Client.LoginAsync(TokenType.Bot, Credentials.Token).ConfigureAwait(false); await Client.ConnectAsync().ConfigureAwait(false); - //await Client.DownloadAllUsersAsync().ConfigureAwait(false); +#if !GLOBAL_NADEKO + await Client.DownloadAllUsersAsync().ConfigureAwait(false); +#endif _log.Info("Connected"); diff --git a/src/NadekoBot/Resources/CommandStrings.Designer.cs b/src/NadekoBot/Resources/CommandStrings.Designer.cs index 70259971..e58dbbbb 100644 --- a/src/NadekoBot/Resources/CommandStrings.Designer.cs +++ b/src/NadekoBot/Resources/CommandStrings.Designer.cs @@ -437,6 +437,33 @@ namespace NadekoBot.Resources { } } + /// + /// Looks up a localized string similar to antilist antilst. + /// + public static string antilist_cmd { + get { + return ResourceManager.GetString("antilist_cmd", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Shows currently enabled protection features.. + /// + public static string antilist_desc { + get { + return ResourceManager.GetString("antilist_desc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to `{0}antilist`. + /// + public static string antilist_usage { + get { + return ResourceManager.GetString("antilist_usage", resourceCulture); + } + } + /// /// Looks up a localized string similar to antiraid. /// @@ -491,6 +518,33 @@ namespace NadekoBot.Resources { } } + /// + /// Looks up a localized string similar to antispamignore. + /// + public static string antispamignore_cmd { + get { + return ResourceManager.GetString("antispamignore_cmd", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Toggles whether antispam ignores current channel. Antispam must be enabled.. + /// + public static string antispamignore_desc { + get { + return ResourceManager.GetString("antispamignore_desc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to `{0}antispamignore`. + /// + public static string antispamignore_usage { + get { + return ResourceManager.GetString("antispamignore_usage", resourceCulture); + } + } + /// /// Looks up a localized string similar to asar. /// @@ -1841,6 +1895,33 @@ namespace NadekoBot.Resources { } } + /// + /// Looks up a localized string similar to createinvite crinv. + /// + public static string createinvite_cmd { + get { + return ResourceManager.GetString("createinvite_cmd", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Creates a new invite which has infinite max uses and never expires.. + /// + public static string createinvite_desc { + get { + return ResourceManager.GetString("createinvite_desc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to `{0}crinv`. + /// + public static string createinvite_usage { + get { + return ResourceManager.GetString("createinvite_usage", resourceCulture); + } + } + /// /// Looks up a localized string similar to createrole cr. /// @@ -3245,33 +3326,6 @@ namespace NadekoBot.Resources { } } - /// - /// Looks up a localized string similar to img i. - /// - public static string i_cmd { - get { - return ResourceManager.GetString("i_cmd", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Pulls the first image found using a search parameter. Use {0}ir for different results.. - /// - public static string i_desc { - get { - return ResourceManager.GetString("i_desc", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to `{0}i cute kitten`. - /// - public static string i_usage { - get { - return ResourceManager.GetString("i_usage", resourceCulture); - } - } - /// /// Looks up a localized string similar to iam. /// @@ -3326,6 +3380,33 @@ namespace NadekoBot.Resources { } } + /// + /// Looks up a localized string similar to image img. + /// + public static string image_cmd { + get { + return ResourceManager.GetString("image_cmd", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Pulls the first image found using a search parameter. Use {0}rimg for different results.. + /// + public static string image_desc { + get { + return ResourceManager.GetString("image_desc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to `{0}img cute kitten`. + /// + public static string image_usage { + get { + return ResourceManager.GetString("image_usage", resourceCulture); + } + } + /// /// Looks up a localized string similar to imdb omdb. /// @@ -3380,33 +3461,6 @@ namespace NadekoBot.Resources { } } - /// - /// Looks up a localized string similar to ir. - /// - public static string ir_cmd { - get { - return ResourceManager.GetString("ir_cmd", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Pulls a random image using a search parameter.. - /// - public static string ir_desc { - get { - return ResourceManager.GetString("ir_desc", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to `{0}ir cute kitten`. - /// - public static string ir_usage { - get { - return ResourceManager.GetString("ir_usage", resourceCulture); - } - } - /// /// Looks up a localized string similar to jcsc. /// @@ -5162,6 +5216,33 @@ namespace NadekoBot.Resources { } } + /// + /// Looks up a localized string similar to pollstats. + /// + public static string pollstats_cmd { + get { + return ResourceManager.GetString("pollstats_cmd", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Shows the poll results without stopping the poll on this server.. + /// + public static string pollstats_desc { + get { + return ResourceManager.GetString("pollstats_desc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to `{0}pollstats`. + /// + public static string pollstats_usage { + get { + return ResourceManager.GetString("pollstats_usage", resourceCulture); + } + } + /// /// Looks up a localized string similar to prune clr. /// @@ -5405,6 +5486,33 @@ namespace NadekoBot.Resources { } } + /// + /// Looks up a localized string similar to randomimage rimg. + /// + public static string randomimage_cmd { + get { + return ResourceManager.GetString("randomimage_cmd", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Pulls a random image using a search parameter.. + /// + public static string randomimage_desc { + get { + return ResourceManager.GetString("randomimage_desc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to `{0}rimg cute kitten`. + /// + public static string randomimage_usage { + get { + return ResourceManager.GetString("randomimage_usage", resourceCulture); + } + } + /// /// Looks up a localized string similar to remind. /// @@ -5658,7 +5766,7 @@ namespace NadekoBot.Resources { } /// - /// Looks up a localized string similar to Repeat a message every X minutes. If no parameters are specified, repeat is disabled.. + /// Looks up a localized string similar to Repeat a message every X minutes in the current channel. You can have up to 5 repeating messages on the server in total.. /// public static string repeat_desc { get { @@ -5685,7 +5793,7 @@ namespace NadekoBot.Resources { } /// - /// Looks up a localized string similar to Immediately shows the repeat message and restarts the timer.. + /// Looks up a localized string similar to Immediately shows the repeat message on a certain index and restarts its timer.. /// public static string repeatinvoke_desc { get { @@ -5694,7 +5802,7 @@ namespace NadekoBot.Resources { } /// - /// Looks up a localized string similar to `{0}repinv`. + /// Looks up a localized string similar to `{0}repinv 1`. /// public static string repeatinvoke_usage { get { @@ -5702,6 +5810,33 @@ namespace NadekoBot.Resources { } } + /// + /// Looks up a localized string similar to repeatlist replst. + /// + public static string repeatlist_cmd { + get { + return ResourceManager.GetString("repeatlist_cmd", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Shows currently repeating messages and their indexes.. + /// + public static string repeatlist_desc { + get { + return ResourceManager.GetString("repeatlist_desc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to `{0}repeatlist`. + /// + public static string repeatlist_usage { + get { + return ResourceManager.GetString("repeatlist_usage", resourceCulture); + } + } + /// /// Looks up a localized string similar to rpeatplaylst rpl. /// @@ -5729,6 +5864,33 @@ namespace NadekoBot.Resources { } } + /// + /// Looks up a localized string similar to repeatremove reprm. + /// + public static string repeatremove_cmd { + get { + return ResourceManager.GetString("repeatremove_cmd", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Removes a repeating message on a specified index. Use `{0}repeatlist` to see indexes.. + /// + public static string repeatremove_desc { + get { + return ResourceManager.GetString("repeatremove_desc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to `{0}reprm 2`. + /// + public static string repeatremove_usage { + get { + return ResourceManager.GetString("repeatremove_usage", resourceCulture); + } + } + /// /// Looks up a localized string similar to reptcursong rcs. /// @@ -6053,6 +6215,33 @@ namespace NadekoBot.Resources { } } + /// + /// Looks up a localized string similar to rotaterolecolor rrc. + /// + public static string rotaterolecolor_cmd { + get { + return ResourceManager.GetString("rotaterolecolor_cmd", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Rotates a roles color on an interval with a list of supplied colors. First argument is interval in seconds (Minimum 60). Second argument is a role, followed by a space-separated list of colors in hex. Provide a rolename with a 0 interval to disable.. + /// + public static string rotaterolecolor_desc { + get { + return ResourceManager.GetString("rotaterolecolor_desc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to `{0}rrc 60 MyLsdRole #ff0000 #00ff00 #0000ff` or `{0}rrc 0 MyLsdRole`. + /// + public static string rotaterolecolor_usage { + get { + return ResourceManager.GetString("rotaterolecolor_usage", resourceCulture); + } + } + /// /// Looks up a localized string similar to rps. /// @@ -6566,6 +6755,33 @@ namespace NadekoBot.Resources { } } + /// + /// Looks up a localized string similar to setstatus. + /// + public static string setstatus_cmd { + get { + return ResourceManager.GetString("setstatus_cmd", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Sets the bot's status. (Online/Idle/Dnd/Invisible). + /// + public static string setstatus_desc { + get { + return ResourceManager.GetString("setstatus_desc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to `{0}setstatus Idle`. + /// + public static string setstatus_usage { + get { + return ResourceManager.GetString("setstatus_usage", resourceCulture); + } + } + /// /// Looks up a localized string similar to setstream. /// diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index fe484754..d2ef3388 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -265,16 +265,16 @@ repeatinvoke repinv - Immediately shows the repeat message and restarts the timer. + Immediately shows the repeat message on a certain index and restarts its timer. - `{0}repinv` + `{0}repinv 1` repeat - Repeat a message every X minutes. If no parameters are specified, repeat is disabled. + Repeat a message every X minutes in the current channel. You can have up to 5 repeating messages on the server in total. `{0}repeat 5 Hello there` @@ -1926,23 +1926,23 @@ `{0}woof` - - img i + + image img - - Pulls the first image found using a search parameter. Use {0}ir for different results. + + Pulls the first image found using a search parameter. Use {0}rimg for different results. - - `{0}i cute kitten` + + `{0}img cute kitten` - - ir + + randomimage rimg - + Pulls a random image using a search parameter. - - `{0}ir cute kitten` + + `{0}rimg cute kitten` lmgtfy @@ -2853,4 +2853,76 @@ `{0}autohentai 30 yuri|tail|long_hair` or `{0}autohentai` + + setstatus + + + Sets the bot's status. (Online/Idle/Dnd/Invisible) + + + `{0}setstatus Idle` + + + rotaterolecolor rrc + + + Rotates a roles color on an interval with a list of supplied colors. First argument is interval in seconds (Minimum 60). Second argument is a role, followed by a space-separated list of colors in hex. Provide a rolename with a 0 interval to disable. + + + `{0}rrc 60 MyLsdRole #ff0000 #00ff00 #0000ff` or `{0}rrc 0 MyLsdRole` + + + createinvite crinv + + + Creates a new invite which has infinite max uses and never expires. + + + `{0}crinv` + + + pollstats + + + Shows the poll results without stopping the poll on this server. + + + `{0}pollstats` + + + repeatlist replst + + + Shows currently repeating messages and their indexes. + + + `{0}repeatlist` + + + repeatremove reprm + + + Removes a repeating message on a specified index. Use `{0}repeatlist` to see indexes. + + + `{0}reprm 2` + + + antilist antilst + + + Shows currently enabled protection features. + + + `{0}antilist` + + + antispamignore + + + Toggles whether antispam ignores current channel. Antispam must be enabled. + + + `{0}antispamignore` + \ No newline at end of file diff --git a/src/NadekoBot/Services/CleverBotApi/ChatterBotFactory.cs b/src/NadekoBot/Services/CleverBotApi/ChatterBotFactory.cs index 36589fe6..08269207 100644 --- a/src/NadekoBot/Services/CleverBotApi/ChatterBotFactory.cs +++ b/src/NadekoBot/Services/CleverBotApi/ChatterBotFactory.cs @@ -29,10 +29,16 @@ namespace Services.CleverBotApi public static ChatterBot Create(ChatterBotType type, object arg) { +#if GLOBAL_NADEKO + var url = "http://www.cleverbot.com/webservicemin?uc=3210&botapi=nadekobot"; +#else + var url = "http://www.cleverbot.com/webservicemin?uc=3210"; +#endif + switch (type) { case ChatterBotType.CLEVERBOT: - return new Cleverbot("http://www.cleverbot.com/", "http://www.cleverbot.com/webservicemin?uc=321", 26); + return new Cleverbot("http://www.cleverbot.com/", url, 26); case ChatterBotType.JABBERWACKY: return new Cleverbot("http://jabberwacky.com", "http://jabberwacky.com/webservicemin", 20); case ChatterBotType.PANDORABOTS: diff --git a/src/NadekoBot/Services/CleverBotApi/Cleverbot.cs b/src/NadekoBot/Services/CleverBotApi/Cleverbot.cs index 5599d7b0..45109863 100644 --- a/src/NadekoBot/Services/CleverBotApi/Cleverbot.cs +++ b/src/NadekoBot/Services/CleverBotApi/Cleverbot.cs @@ -46,7 +46,7 @@ namespace Services.CleverBotApi private readonly int endIndex; private readonly string url; private readonly IDictionary vars; - private readonly CookieCollection cookies; + private readonly CookieCollection cookies; public CleverbotSession(string baseUrl, string url, int endIndex) { @@ -60,7 +60,7 @@ namespace Services.CleverBotApi //vars["fno"] = "0"; //vars["sub"] = "Say"; //vars["cleanslate"] = "false"; - cookies = Utils.GetCookies(baseUrl); + cookies = Utils.GetCookies(baseUrl); } public async Task Think(ChatterBotThought thought) diff --git a/src/NadekoBot/Services/CommandHandler.cs b/src/NadekoBot/Services/CommandHandler.cs index 60196598..e4e30c4d 100644 --- a/src/NadekoBot/Services/CommandHandler.cs +++ b/src/NadekoBot/Services/CommandHandler.cs @@ -17,6 +17,8 @@ using static NadekoBot.Modules.Administration.Administration; using NadekoBot.Modules.CustomReactions; using NadekoBot.Modules.Games; using System.Collections.Concurrent; +using System.Threading; +using NadekoBot.DataStructures; namespace NadekoBot.Services { @@ -28,6 +30,8 @@ namespace NadekoBot.Services } public class CommandHandler { + public const int GlobalCommandsCooldown = 1500; + private ShardedDiscordClient _client; private CommandService _commandService; private Logger _log; @@ -39,11 +43,19 @@ namespace NadekoBot.Services //userid/msg count public ConcurrentDictionary UserMessagesSent { get; } = new ConcurrentDictionary(); + public ConcurrentHashSet UsersOnShortCooldown { get; } = new ConcurrentHashSet(); + private Timer clearUsersOnShortCooldown { get; } + public CommandHandler(ShardedDiscordClient client, CommandService commandService) { _client = client; _commandService = commandService; _log = LogManager.GetCurrentClassLogger(); + + clearUsersOnShortCooldown = new Timer((_) => + { + UsersOnShortCooldown.Clear(); + }, null, GlobalCommandsCooldown, GlobalCommandsCooldown); } public async Task StartHandling() { @@ -62,158 +74,202 @@ namespace NadekoBot.Services _client.MessageReceived += MessageReceivedHandler; } - private async void MessageReceivedHandler(SocketMessage msg) + private async Task TryRunCleverbot(SocketUserMessage usrMsg, IGuild guild) { + if (guild == null) + return false; try { - - var usrMsg = msg as SocketUserMessage; - if (usrMsg == null) - return; - - if (!usrMsg.IsAuthor()) - UserMessagesSent.AddOrUpdate(usrMsg.Author.Id, 1, (key, old) => ++old); - - if (msg.Author.IsBot || !NadekoBot.Ready) //no bots - return; - - var guild = (msg.Channel as SocketTextChannel)?.Guild; - - if (guild != null && guild.OwnerId != msg.Author.Id) + var cleverbotExecuted = await Games.CleverBotCommands.TryAsk(usrMsg).ConfigureAwait(false); + if (cleverbotExecuted) { - //todo split checks into their own modules - if (Permissions.FilterCommands.InviteFilteringChannels.Contains(msg.Channel.Id) || - Permissions.FilterCommands.InviteFilteringServers.Contains(guild.Id)) - { - if (usrMsg.Content.IsDiscordInvite()) - { - try - { - await usrMsg.DeleteAsync().ConfigureAwait(false); - return; - } - catch (HttpException ex) - { - _log.Warn("I do not have permission to filter invites in channel with id " + msg.Channel.Id, ex); - } - } - } - - var filteredWords = Permissions.FilterCommands.FilteredWordsForChannel(msg.Channel.Id, guild.Id).Concat(Permissions.FilterCommands.FilteredWordsForServer(guild.Id)); - var wordsInMessage = usrMsg.Content.ToLowerInvariant().Split(' '); - if (filteredWords.Any(w => wordsInMessage.Contains(w))) - { - try - { - await usrMsg.DeleteAsync().ConfigureAwait(false); - return; - } - catch (HttpException ex) - { - _log.Warn("I do not have permission to filter words in channel with id " + msg.Channel.Id, ex); - } - } + _log.Info($@"CleverBot Executed + Server: {guild.Name} [{guild.Id}] + Channel: {usrMsg.Channel?.Name} [{usrMsg.Channel?.Id}] + UserId: {usrMsg.Author} [{usrMsg.Author.Id}] + Message: {usrMsg.Content}"); + return true; } + } + catch (Exception ex) { _log.Warn(ex, "Error in cleverbot"); } + return false; + } - BlacklistItem blacklistedItem; - if ((blacklistedItem = Permissions.BlacklistCommands.BlacklistedItems.FirstOrDefault(bi => - (bi.Type == BlacklistItem.BlacklistType.Server && bi.ItemId == guild?.Id) || - (bi.Type == BlacklistItem.BlacklistType.Channel && bi.ItemId == msg.Channel.Id) || - (bi.Type == BlacklistItem.BlacklistType.User && bi.ItemId == msg.Author.Id))) != null) - { - return; - } -#if !GLOBAL_NADEKO - try - { - var cleverbotExecuted = await Games.CleverBotCommands.TryAsk(usrMsg); + private bool IsBlacklisted(IGuild guild, SocketUserMessage usrMsg) => + (guild != null && BlacklistCommands.BlacklistedGuilds.Contains(guild.Id)) || + BlacklistCommands.BlacklistedChannels.Contains(usrMsg.Channel.Id) || + BlacklistCommands.BlacklistedUsers.Contains(usrMsg.Author.Id); - if (cleverbotExecuted) - return; - } - catch (Exception ex) { _log.Warn(ex, "Error in cleverbot"); } -#endif - try - { - // maybe this message is a custom reaction - var crExecuted = await CustomReactions.TryExecuteCustomReaction(usrMsg).ConfigureAwait(false); + private async Task LogSuccessfulExecution(SocketUserMessage usrMsg, ExecuteCommandResult exec, SocketTextChannel channel, Stopwatch sw) + { + await CommandExecuted(usrMsg, exec.CommandInfo).ConfigureAwait(false); + _log.Info("Command Executed after {4}s\n\t" + + "User: {0}\n\t" + + "Server: {1}\n\t" + + "Channel: {2}\n\t" + + "Message: {3}", + usrMsg.Author + " [" + usrMsg.Author.Id + "]", // {0} + (channel == null ? "PRIVATE" : channel.Guild.Name + " [" + channel.Guild.Id + "]"), // {1} + (channel == null ? "PRIVATE" : channel.Name + " [" + channel.Id + "]"), // {2} + usrMsg.Content, // {3} + sw.Elapsed.TotalSeconds); + } - //if it was, don't execute the command - if (crExecuted) - return; - } - catch { } - - string messageContent = usrMsg.Content; - - var sw = new Stopwatch(); - sw.Start(); - var exec = await ExecuteCommand(new CommandContext(_client.MainClient, usrMsg), messageContent, DependencyMap.Empty, MultiMatchHandling.Best); - var command = exec.CommandInfo; - var permCache = exec.PermissionCache; - var result = exec.Result; - sw.Stop(); - var channel = (msg.Channel as ITextChannel); - if (result.IsSuccess) - { - await CommandExecuted(usrMsg, command); - _log.Info("Command Executed after {4}s\n\t" + - "User: {0}\n\t" + - "Server: {1}\n\t" + - "Channel: {2}\n\t" + - "Message: {3}", - msg.Author + " [" + msg.Author.Id + "]", // {0} - (channel == null ? "PRIVATE" : channel.Guild.Name + " [" + channel.Guild.Id + "]"), // {1} - (channel == null ? "PRIVATE" : channel.Name + " [" + channel.Id + "]"), // {2} - usrMsg.Content, // {3} - sw.Elapsed.TotalSeconds // {4} - ); - } - else if (!result.IsSuccess && result.Error != CommandError.UnknownCommand) - { - _log.Warn("Command Errored after {5}s\n\t" + + private void LogErroredExecution(SocketUserMessage usrMsg, ExecuteCommandResult exec, SocketTextChannel channel, Stopwatch sw) + { + _log.Warn("Command Errored after {5}s\n\t" + "User: {0}\n\t" + "Server: {1}\n\t" + "Channel: {2}\n\t" + "Message: {3}\n\t" + "Error: {4}", - msg.Author + " [" + msg.Author.Id + "]", // {0} + usrMsg.Author + " [" + usrMsg.Author.Id + "]", // {0} (channel == null ? "PRIVATE" : channel.Guild.Name + " [" + channel.Guild.Id + "]"), // {1} (channel == null ? "PRIVATE" : channel.Name + " [" + channel.Id + "]"), // {2} usrMsg.Content,// {3} - result.ErrorReason, // {4} + exec.Result.ErrorReason, // {4} sw.Elapsed.TotalSeconds // {5} ); - if (guild != null && command != null && result.Error == CommandError.Exception) + } + + private async Task InviteFiltered(IGuild guild, SocketUserMessage usrMsg) + { + if ((Permissions.FilterCommands.InviteFilteringChannels.Contains(usrMsg.Channel.Id) || + Permissions.FilterCommands.InviteFilteringServers.Contains(guild.Id)) && + usrMsg.Content.IsDiscordInvite()) + { + try + { + await usrMsg.DeleteAsync().ConfigureAwait(false); + return true; + } + catch (HttpException ex) + { + _log.Warn("I do not have permission to filter invites in channel with id " + usrMsg.Channel.Id, ex); + return true; + } + } + return false; + } + + private async Task WordFiltered(IGuild guild, SocketUserMessage usrMsg) + { + var filteredChannelWords = Permissions.FilterCommands.FilteredWordsForChannel(usrMsg.Channel.Id, guild.Id); + var filteredServerWords = Permissions.FilterCommands.FilteredWordsForServer(guild.Id); + var wordsInMessage = usrMsg.Content.ToLowerInvariant().Split(' '); + if (filteredChannelWords.Count != 0 || filteredServerWords.Count != 0) + { + foreach (var word in wordsInMessage) + { + if (filteredChannelWords.Contains(word) || + filteredServerWords.Contains(word)) { - if (permCache != null && permCache.Verbose) - try { await msg.Channel.SendMessageAsync("⚠️ " + result.ErrorReason).ConfigureAwait(false); } catch { } + try + { + await usrMsg.DeleteAsync().ConfigureAwait(false); + } + catch (HttpException ex) + { + _log.Warn("I do not have permission to filter words in channel with id " + usrMsg.Channel.Id, ex); + } + return true; + } + } + } + return false; + } + + private async void MessageReceivedHandler(SocketMessage msg) + { + try + { + if (msg.Author.IsBot || !NadekoBot.Ready) //no bots, wait until bot connected and initialized + return; + + var usrMsg = msg as SocketUserMessage; + if (usrMsg == null) //has to be an user message, not system/other messages. + return; + + // track how many messagges each user is sending + UserMessagesSent.AddOrUpdate(usrMsg.Author.Id, 1, (key, old) => ++old); + + // Bot will ignore commands which are ran more often than what specified by + // GlobalCommandsCooldown constant (miliseconds) + if (!UsersOnShortCooldown.Add(usrMsg.Author.Id)) + return; + + var channel = msg.Channel as SocketTextChannel; + var guild = channel?.Guild; + + if (guild != null && guild.OwnerId != msg.Author.Id) + { + if (await InviteFiltered(guild, usrMsg).ConfigureAwait(false)) + return; + + if (await WordFiltered(guild, usrMsg).ConfigureAwait(false)) + return; + } + + if (IsBlacklisted(guild, usrMsg)) + return; + + var cleverBotRan = await TryRunCleverbot(usrMsg, guild).ConfigureAwait(false); + if (cleverBotRan) + return; + + // maybe this message is a custom reaction + var crExecuted = await CustomReactions.TryExecuteCustomReaction(usrMsg).ConfigureAwait(false); + if (crExecuted) //if it was, don't execute the command + return; + + string messageContent = usrMsg.Content; + + // execute the command and measure the time it took + var sw = Stopwatch.StartNew(); + var exec = await ExecuteCommand(new CommandContext(_client.MainClient, usrMsg), messageContent, DependencyMap.Empty, MultiMatchHandling.Best); + sw.Stop(); + + if (exec.Result.IsSuccess) + { + await LogSuccessfulExecution(usrMsg, exec, channel, sw).ConfigureAwait(false); + } + else if (!exec.Result.IsSuccess && exec.Result.Error != CommandError.UnknownCommand) + { + LogErroredExecution(usrMsg, exec, channel, sw); + if (guild != null && exec.CommandInfo != null && exec.Result.Error == CommandError.Exception) + { + if (exec.PermissionCache != null && exec.PermissionCache.Verbose) + try { await msg.Channel.SendMessageAsync("⚠️ " + exec.Result.ErrorReason).ConfigureAwait(false); } catch { } } } else { if (msg.Channel is IPrivateChannel) { - //rofl, gotta do this to prevent this message from occuring on polls + // rofl, gotta do this to prevent dm help message being sent to + // users who are voting on private polls (sending a number in a DM) int vote; if (int.TryParse(msg.Content, out vote)) return; - + await msg.Channel.SendMessageAsync(Help.DMHelpString).ConfigureAwait(false); - await DMForwardCommands.HandleDMForwarding(msg, ownerChannels); + await DMForwardCommands.HandleDMForwarding(msg, ownerChannels).ConfigureAwait(false); } } } catch (Exception ex) { - _log.Warn(ex, "Error in CommandHandler"); + _log.Warn("Error in CommandHandler"); + _log.Warn(ex); if (ex.InnerException != null) - _log.Warn(ex.InnerException, "Inner Exception of the error in CommandHandler"); + { + _log.Warn("Inner Exception of the error in CommandHandler"); + _log.Warn(ex.InnerException); + } } - - return; } + public Task ExecuteCommandAsync(CommandContext context, int argPos, IDependencyMap dependencyMap = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) => ExecuteCommand(context, context.Message.Content.Substring(argPos), dependencyMap, multiMatchHandling); @@ -308,19 +364,5 @@ namespace NadekoBot.Services return new ExecuteCommandResult(null, null, SearchResult.FromError(CommandError.UnknownCommand, "This input does not match any overload.")); } - - public struct ExecuteCommandResult - { - public readonly CommandInfo CommandInfo; - public readonly PermissionCache PermissionCache; - public readonly IResult Result; - - public ExecuteCommandResult(CommandInfo commandInfo, PermissionCache cache, IResult result) - { - this.CommandInfo = commandInfo; - this.PermissionCache = cache; - this.Result = result; - } - } } } \ No newline at end of file diff --git a/src/NadekoBot/Services/Database/IUnitOfWork.cs b/src/NadekoBot/Services/Database/IUnitOfWork.cs index c90d9736..f17e1f6a 100644 --- a/src/NadekoBot/Services/Database/IUnitOfWork.cs +++ b/src/NadekoBot/Services/Database/IUnitOfWork.cs @@ -15,7 +15,6 @@ namespace NadekoBot.Services.Database IReminderRepository Reminders { get; } ISelfAssignedRolesRepository SelfAssignedRoles { get; } IBotConfigRepository BotConfig { get; } - IRepeaterRepository Repeaters { get; } IUnitConverterRepository ConverterUnits { get; } ICustomReactionRepository CustomReactions { get; } ICurrencyRepository Currency { get; } diff --git a/src/NadekoBot/Services/Database/Models/AntiProtection.cs b/src/NadekoBot/Services/Database/Models/AntiProtection.cs new file mode 100644 index 00000000..0172dd90 --- /dev/null +++ b/src/NadekoBot/Services/Database/Models/AntiProtection.cs @@ -0,0 +1,53 @@ +using Discord; +using NadekoBot.Services.Database.Models; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +namespace NadekoBot.Services.Database.Models +{ + public class AntiRaidSetting : DbEntity + { + public int GuildConfigId { get; set; } + public GuildConfig GuildConfig { get; set; } + + public int UserThreshold { get; set; } + public int Seconds { get; set; } + public PunishmentAction Action { get; set; } + } + + public class AntiSpamSetting : DbEntity + { + public int GuildConfigId { get; set; } + public GuildConfig GuildConfig { get; set; } + + public PunishmentAction Action { get; set; } + public int MessageThreshold { get; set; } = 3; + public HashSet IgnoredChannels { get; set; } = new HashSet(); + } + + + public enum PunishmentAction + { + Mute, + Kick, + Ban, + } + + public class AntiSpamIgnore : DbEntity + { + public ulong ChannelId { get; set; } + + public override int GetHashCode() => ChannelId.GetHashCode(); + + public override bool Equals(object obj) + { + var inst = obj as AntiSpamIgnore; + + if (inst == null) + return false; + + return inst.ChannelId == ChannelId; + + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Services/Database/Models/GuildConfig.cs b/src/NadekoBot/Services/Database/Models/GuildConfig.cs index 99608eff..7931e8c9 100644 --- a/src/NadekoBot/Services/Database/Models/GuildConfig.cs +++ b/src/NadekoBot/Services/Database/Models/GuildConfig.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using static NadekoBot.Modules.Administration.Administration; namespace NadekoBot.Services.Database.Models { @@ -58,6 +59,12 @@ namespace NadekoBot.Services.Database.Models public string MuteRoleName { get; set; } public bool CleverbotEnabled { get; set; } + public HashSet GuildRepeaters { get; set; } = new HashSet(); + + public AntiRaidSetting AntiRaidSetting { get; set; } + public AntiSpamSetting AntiSpamSetting { get; set; } + + //public List ProtectionIgnoredChannels { get; set; } = new List(); } public class FilterChannelId : DbEntity diff --git a/src/NadekoBot/Services/Database/Models/Repeater.cs b/src/NadekoBot/Services/Database/Models/Repeater.cs index cf887b43..f8c07bfe 100644 --- a/src/NadekoBot/Services/Database/Models/Repeater.cs +++ b/src/NadekoBot/Services/Database/Models/Repeater.cs @@ -2,11 +2,16 @@ namespace NadekoBot.Services.Database.Models { - public class Repeater :DbEntity + public class Repeater : DbEntity { public ulong GuildId { get; set; } public ulong ChannelId { get; set; } public string Message { get; set; } public TimeSpan Interval { get; set; } } + + public class GuildRepeater : Repeater + { + + } } diff --git a/src/NadekoBot/Services/Database/NadekoContext.cs b/src/NadekoBot/Services/Database/NadekoContext.cs index ec7c2288..e955409f 100644 --- a/src/NadekoBot/Services/Database/NadekoContext.cs +++ b/src/NadekoBot/Services/Database/NadekoContext.cs @@ -16,7 +16,6 @@ namespace NadekoBot.Services.Database public DbSet Reminders { get; set; } public DbSet SelfAssignableRoles { get; set; } public DbSet BotConfig { get; set; } - public DbSet Repeaters { get; set; } public DbSet Currency { get; set; } public DbSet ConversionUnits { get; set; } public DbSet MusicPlaylists { get; set; } @@ -44,6 +43,7 @@ namespace NadekoBot.Services.Database this.Database.Migrate(); EnsureSeedData(); } + ////Uncomment this to db initialisation with dotnet ef migration add [module] //protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) //{ @@ -142,6 +142,17 @@ namespace NadekoBot.Services.Database .HasIndex(c => c.GuildId) .IsUnique(); + modelBuilder.Entity() + .HasOne(x => x.GuildConfig) + .WithOne(x => x.AntiSpamSetting); + + modelBuilder.Entity() + .HasOne(x => x.GuildConfig) + .WithOne(x => x.AntiRaidSetting); + + //modelBuilder.Entity() + // .HasAlternateKey(c => new { c.ChannelId, c.ProtectionType }); + #endregion #region BotConfig @@ -172,16 +183,6 @@ namespace NadekoBot.Services.Database #endregion - #region Repeater - - var repeaterEntity = modelBuilder.Entity(); - - repeaterEntity - .HasIndex(r => r.ChannelId) - .IsUnique(); - - #endregion - #region Currency var currencyEntity = modelBuilder.Entity(); @@ -231,6 +232,11 @@ namespace NadekoBot.Services.Database .IsUnique(); + #endregion + + #region Protection + + #endregion } } diff --git a/src/NadekoBot/Services/Database/Repositories/IRepeaterRepository.cs b/src/NadekoBot/Services/Database/Repositories/IRepeaterRepository.cs deleted file mode 100644 index 2446c275..00000000 --- a/src/NadekoBot/Services/Database/Repositories/IRepeaterRepository.cs +++ /dev/null @@ -1,9 +0,0 @@ -using NadekoBot.Services.Database.Models; - -namespace NadekoBot.Services.Database.Repositories -{ - public interface IRepeaterRepository : IRepository - { - - } -} diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/GuildConfigRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/GuildConfigRepository.cs index 0720e8ad..89c59419 100644 --- a/src/NadekoBot/Services/Database/Repositories/Impl/GuildConfigRepository.cs +++ b/src/NadekoBot/Services/Database/Repositories/Impl/GuildConfigRepository.cs @@ -26,6 +26,10 @@ namespace NadekoBot.Services.Database.Repositories.Impl .Include(gc => gc.FilterWordsChannelIds) .Include(gc => gc.FilteredWords) .Include(gc => gc.CommandCooldowns) + .Include(gc => gc.GuildRepeaters) + .Include(gc => gc.AntiRaidSetting) + .Include(gc => gc.AntiSpamSetting) + .ThenInclude(x => x.IgnoredChannels) .ToList(); /// diff --git a/src/NadekoBot/Services/Database/Repositories/Impl/RepeaterRepository.cs b/src/NadekoBot/Services/Database/Repositories/Impl/RepeaterRepository.cs deleted file mode 100644 index 94827f95..00000000 --- a/src/NadekoBot/Services/Database/Repositories/Impl/RepeaterRepository.cs +++ /dev/null @@ -1,12 +0,0 @@ -using NadekoBot.Services.Database.Models; -using Microsoft.EntityFrameworkCore; - -namespace NadekoBot.Services.Database.Repositories.Impl -{ - public class RepeaterRepository : Repository, IRepeaterRepository - { - public RepeaterRepository(DbContext context) : base(context) - { - } - } -} diff --git a/src/NadekoBot/Services/Database/UnitOfWork.cs b/src/NadekoBot/Services/Database/UnitOfWork.cs index 9601145d..88231d6b 100644 --- a/src/NadekoBot/Services/Database/UnitOfWork.cs +++ b/src/NadekoBot/Services/Database/UnitOfWork.cs @@ -30,9 +30,6 @@ namespace NadekoBot.Services.Database private IBotConfigRepository _botConfig; public IBotConfigRepository BotConfig => _botConfig ?? (_botConfig = new BotConfigRepository(_context)); - private IRepeaterRepository _repeaters; - public IRepeaterRepository Repeaters => _repeaters ?? (_repeaters = new RepeaterRepository(_context)); - private ICurrencyRepository _currency; public ICurrencyRepository Currency => _currency ?? (_currency = new CurrencyRepository(_context)); diff --git a/src/NadekoBot/Services/Discord/SocketMessageEventWrapper.cs b/src/NadekoBot/Services/Discord/SocketMessageEventWrapper.cs new file mode 100644 index 00000000..b73961a0 --- /dev/null +++ b/src/NadekoBot/Services/Discord/SocketMessageEventWrapper.cs @@ -0,0 +1,78 @@ +using Discord; +using Discord.WebSocket; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.Services.Discord +{ + public class ReactionEventWrapper : IDisposable + { + public IUserMessage Message { get; } + public event Action OnReactionAdded = delegate { }; + public event Action OnReactionRemoved = delegate { }; + public event Action OnReactionsCleared = delegate { }; + + public ReactionEventWrapper(IUserMessage msg) + { + if (msg == null) + throw new ArgumentNullException(nameof(msg)); + Message = msg; + + NadekoBot.Client.ReactionAdded += Discord_ReactionAdded; + NadekoBot.Client.ReactionRemoved += Discord_ReactionRemoved; + NadekoBot.Client.ReactionsCleared += Discord_ReactionsCleared; + } + + private void Discord_ReactionsCleared(ulong messageId, Optional reaction) + { + try + { + if (messageId == Message.Id) + OnReactionsCleared?.Invoke(); + } + catch { } + } + + private void Discord_ReactionRemoved(ulong messageId, Optional arg2, SocketReaction reaction) + { + try + { + if (messageId == Message.Id) + OnReactionRemoved?.Invoke(reaction); + } + catch { } + } + + private void Discord_ReactionAdded(ulong messageId, Optional message, SocketReaction reaction) + { + try + { + if (messageId == Message.Id) + OnReactionAdded?.Invoke(reaction); + } + catch { } + } + + public void UnsubAll() + { + NadekoBot.Client.ReactionAdded -= Discord_ReactionAdded; + NadekoBot.Client.ReactionRemoved -= Discord_ReactionRemoved; + NadekoBot.Client.ReactionsCleared -= Discord_ReactionsCleared; + OnReactionAdded = null; + OnReactionRemoved = null; + OnReactionsCleared = null; + } + + private bool disposing = false; + public void Dispose() + { + if (disposing) + return; + disposing = true; + UnsubAll(); + } + } +} diff --git a/src/NadekoBot/Services/Impl/GoogleApiService.cs b/src/NadekoBot/Services/Impl/GoogleApiService.cs index 6e5727f7..eaa066ff 100644 --- a/src/NadekoBot/Services/Impl/GoogleApiService.cs +++ b/src/NadekoBot/Services/Impl/GoogleApiService.cs @@ -51,7 +51,7 @@ namespace NadekoBot.Services.Impl return (await query.ExecuteAsync()).Items.Select(i => i.Id.PlaylistId); } - private readonly Regex YtVideoIdRegex = new Regex("(?:youtu\\.be\\/|v\\/|u\\/\\w\\/|embed\\/|watch\\?v=|\\&v=)(?[^#\\&\\?]*)", RegexOptions.Compiled); + private readonly Regex YtVideoIdRegex = new Regex(@"(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\?(?:\S*?&?v\=))|youtu\.be\/)([a-zA-Z0-9_-]{6,11})", RegexOptions.Compiled); public async Task> GetRelatedVideosAsync(string id, int count = 1) { @@ -149,7 +149,7 @@ namespace NadekoBot.Services.Impl return toReturn; } - + //todo AsyncEnumerable public async Task> GetVideoDurationsAsync(IEnumerable videoIds) { var videoIdsList = videoIds as List ?? videoIds.ToList(); diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index c767ecec..c6d22070 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -14,18 +14,21 @@ namespace NadekoBot.Services.Impl private ShardedDiscordClient client; private DateTime started; - public const string BotVersion = "1.1.0-beta"; + public const string BotVersion = "1.1.0"; public string Author => "Kwoth#2560"; public string Library => "Discord.Net"; public int MessageCounter { get; private set; } = 0; public int CommandsRan { get; private set; } = 0; - public string Heap => Math.Round((double)GC.GetTotalMemory(false) / 1.MiB(), 2).ToString(); - public double MessagesPerSecond => MessageCounter / (double)GetUptime().TotalSeconds; - public int TextChannels => client.GetGuilds().SelectMany(g => g.Channels.Where(c => c is ITextChannel)).Count(); - public int VoiceChannels => client.GetGuilds().SelectMany(g => g.Channels.Where(c => c is IVoiceChannel)).Count(); + public string Heap => + Math.Round((double)GC.GetTotalMemory(false) / 1.MiB(), 2).ToString(); + public double MessagesPerSecond => MessageCounter / GetUptime().TotalSeconds; + private int _textChannels = 0; + public int TextChannels => _textChannels; + private int _voiceChannels = 0; + public int VoiceChannels => _voiceChannels; public string OwnerIds => string.Join(", ", NadekoBot.Credentials.OwnerIds); - + Timer carbonitexTimer { get; } public StatsService(ShardedDiscordClient client, CommandHandler cmdHandler) @@ -39,17 +42,56 @@ namespace NadekoBot.Services.Impl this.client.Disconnected += _ => Reset(); + this.client.Connected += () => + { + var guilds = this.client.GetGuilds(); + _textChannels = guilds.Sum(g => g.Channels.Where(cx => cx is ITextChannel).Count()); + _voiceChannels = guilds.Sum(g => g.Channels.Count) - _textChannels; + }; + + this.client.ChannelCreated += (c) => + { + if (c is ITextChannel) + ++_textChannels; + else if (c is IVoiceChannel) + ++_voiceChannels; + }; + + this.client.ChannelDestroyed += (c) => + { + if (c is ITextChannel) + --_textChannels; + else if (c is IVoiceChannel) + --_voiceChannels; + }; + + this.client.JoinedGuild += (g) => + { + var tc = g.Channels.Where(cx => cx is ITextChannel).Count(); + var vc = g.Channels.Count - tc; + _textChannels += tc; + _voiceChannels += vc; + }; + + this.client.LeftGuild += (g) => + { + var tc = g.Channels.Where(cx => cx is ITextChannel).Count(); + var vc = g.Channels.Count - tc; + _textChannels -= tc; + _voiceChannels -= vc; + }; + this.carbonitexTimer = new Timer(async (state) => { - if (string.IsNullOrWhiteSpace(NadekoBot.Credentials.CarbonKey)) - return; + if (string.IsNullOrWhiteSpace(NadekoBot.Credentials.CarbonKey)) + return; try { using (var http = new HttpClient()) { using (var content = new FormUrlEncodedContent( new Dictionary { - { "servercount", this.client.GetGuilds().Count.ToString() }, + { "servercount", this.client.GetGuildsCount().ToString() }, { "key", NadekoBot.Credentials.CarbonKey }})) { content.Headers.Clear(); @@ -71,7 +113,7 @@ Bot Version: [{BotVersion}] Bot ID: {curUser.Id} Owner ID(s): {OwnerIds} Uptime: {GetUptimeString()} -Servers: {client.GetGuilds().Count} | TextChannels: {TextChannels} | VoiceChannels: {VoiceChannels} +Servers: {client.GetGuildsCount()} | TextChannels: {TextChannels} | VoiceChannels: {VoiceChannels} Commands Ran this session: {CommandsRan} Messages: {MessageCounter} [{MessagesPerSecond:F2}/sec] Heap: [{Heap} MB]"); } diff --git a/src/NadekoBot/ShardedDiscordClient.cs b/src/NadekoBot/ShardedDiscordClient.cs index 8a341197..a59dece7 100644 --- a/src/NadekoBot/ShardedDiscordClient.cs +++ b/src/NadekoBot/ShardedDiscordClient.cs @@ -18,6 +18,7 @@ namespace NadekoBot public event Action MessageReceived = delegate { }; public event Action UserLeft = delegate { }; public event Action UserUpdated = delegate { }; + public event Action GuildUserUpdated = delegate { }; public event Action, SocketMessage> MessageUpdated = delegate { }; public event Action> MessageDeleted = delegate { }; public event Action UserBanned = delegate { }; @@ -27,11 +28,21 @@ namespace NadekoBot public event Action ChannelCreated = delegate { }; public event Action ChannelDestroyed = delegate { }; public event Action ChannelUpdated = delegate { }; + public event Action, SocketReaction> ReactionAdded = delegate { }; + public event Action, SocketReaction> ReactionRemoved = delegate { }; + public event Action> ReactionsCleared = delegate { }; + + public event Action JoinedGuild = delegate { }; + public event Action LeftGuild = delegate { }; + public event Action Disconnected = delegate { }; + public event Action Connected = delegate { }; private uint _connectedCount = 0; private uint _downloadedCount = 0; + private int _guildCount = 0; + private IReadOnlyList Clients { get; } public ShardedDiscordClient(DiscordSocketConfig discordSocketConfig) @@ -54,6 +65,7 @@ namespace NadekoBot }; client.UserLeft += arg1 => { UserLeft(arg1); return Task.CompletedTask; }; client.UserUpdated += (arg1, gu2) => { UserUpdated(arg1, gu2); return Task.CompletedTask; }; + client.GuildMemberUpdated += (arg1, arg2) => { GuildUserUpdated(arg1, arg2); return Task.CompletedTask; }; client.MessageUpdated += (arg1, m2) => { MessageUpdated(arg1, m2); return Task.CompletedTask; }; client.MessageDeleted += (arg1, arg2) => { MessageDeleted(arg1, arg2); return Task.CompletedTask; }; client.UserBanned += (arg1, arg2) => { UserBanned(arg1, arg2); return Task.CompletedTask; }; @@ -63,30 +75,69 @@ namespace NadekoBot client.ChannelCreated += arg => { ChannelCreated(arg); return Task.CompletedTask; }; client.ChannelDestroyed += arg => { ChannelDestroyed(arg); return Task.CompletedTask; }; client.ChannelUpdated += (arg1, arg2) => { ChannelUpdated(arg1, arg2); return Task.CompletedTask; }; + client.JoinedGuild += (arg1) => { JoinedGuild(arg1); ++_guildCount; return Task.CompletedTask; }; + client.LeftGuild += (arg1) => { LeftGuild(arg1); --_guildCount; return Task.CompletedTask; }; + client.ReactionAdded += (arg1, arg2, arg3) => { ReactionAdded(arg1, arg2, arg3); return Task.CompletedTask; }; + client.ReactionRemoved += (arg1, arg2, arg3) => { ReactionRemoved(arg1, arg2, arg3); return Task.CompletedTask; }; + client.ReactionsCleared += (arg1, arg2) => { ReactionsCleared(arg1, arg2); return Task.CompletedTask; }; _log.Info($"Shard #{i} initialized."); +#if GLOBAL_NADEKO + client.Log += Client_Log; +#endif + var j = i; + client.Disconnected += (ex) => + { + _log.Error("Shard #{0} disconnected", j); + _log.Error(ex, ex?.Message ?? "No error"); + return Task.CompletedTask; + }; } Clients = clientList.AsReadOnly(); } + private Task Client_Log(LogMessage arg) + { + _log.Warn(arg.Message); + _log.Error(arg.Exception); + return Task.CompletedTask; + } + public DiscordSocketClient MainClient => Clients[0]; public SocketSelfUser CurrentUser() => Clients[0].CurrentUser; - public IReadOnlyCollection GetGuilds() => - Clients.SelectMany(c => c.Guilds).ToList(); + public IEnumerable GetGuilds() => + Clients.SelectMany(c => c.Guilds); - public SocketGuild GetGuild(ulong id) => - Clients.Select(c => c.GetGuild(id)).FirstOrDefault(g => g != null); + public int GetGuildsCount() => + _guildCount; + + public SocketGuild GetGuild(ulong id) + { + foreach (var c in Clients) + { + var g = c.GetGuild(id); + if (g != null) + return g; + } + return null; + } public Task GetDMChannelAsync(ulong channelId) => Clients[0].GetDMChannelAsync(channelId); - internal Task LoginAsync(TokenType tokenType, string token) => - Task.WhenAll(Clients.Select(async c => { await c.LoginAsync(tokenType, token).ConfigureAwait(false); _log.Info($"Shard #{c.ShardId} logged in."); })); + internal async Task LoginAsync(TokenType tokenType, string token) + { + foreach (var c in Clients) + { + await c.LoginAsync(tokenType, token).ConfigureAwait(false); + _log.Info($"Shard #{c.ShardId} logged in."); + } + } internal async Task ConnectAsync() { @@ -99,6 +150,7 @@ namespace NadekoBot await c.ConnectAsync().ConfigureAwait(false); sw.Stop(); _log.Info($"Shard #{c.ShardId} connected after {sw.Elapsed.TotalSeconds:F2}s ({++_connectedCount}/{Clients.Count})"); + _guildCount += c.Guilds.Count; } catch { @@ -111,6 +163,7 @@ namespace NadekoBot } } } + Connected(); } internal Task DownloadAllUsersAsync() => @@ -126,5 +179,37 @@ namespace NadekoBot public Task SetStream(string name, string url) => Task.WhenAll(Clients.Select(ms => ms.SetGameAsync(name, url, StreamType.NotStreaming))); + + public Task SetStatus(SettableUserStatus status) => Task.WhenAll(Clients.Select(ms => ms.SetStatusAsync(SettableUserStatusToUserStatus(status)))); + + private static UserStatus SettableUserStatusToUserStatus(SettableUserStatus sus) + { + switch (sus) + { + case SettableUserStatus.Online: + return UserStatus.Online; + case SettableUserStatus.Invisible: + return UserStatus.Invisible; + case SettableUserStatus.Idle: + return UserStatus.AFK; + case SettableUserStatus.Dnd: + return UserStatus.DoNotDisturb; + } + + return UserStatus.Online; + } + } + + public enum SettableUserStatus + { + Online = 1, + On = 1, + Invisible = 2, + Invis = 2, + Idle = 3, + Afk = 3, + Dnd = 4, + DoNotDisturb = 4, + Busy = 4, } } \ No newline at end of file diff --git a/src/NadekoBot/_Extensions/Extensions.cs b/src/NadekoBot/_Extensions/Extensions.cs index f9a8157b..310b5974 100644 --- a/src/NadekoBot/_Extensions/Extensions.cs +++ b/src/NadekoBot/_Extensions/Extensions.cs @@ -1,6 +1,8 @@ using Discord; using Discord.Commands; +using Discord.WebSocket; using ImageSharp; +using NadekoBot.Services.Discord; using Newtonsoft.Json; using System; using System.Collections.Concurrent; @@ -16,6 +18,73 @@ namespace NadekoBot.Extensions { public static class Extensions { + private const string arrow_left = "⬅"; + private const string arrow_right = "➡"; + + /// + /// danny kamisama + /// + public static async Task SendPaginatedConfirmAsync(this IMessageChannel channel, int currentPage, Func pageFunc, int? lastPage = null) + { + lastPage += 1; + var embed = pageFunc(currentPage).AddPaginatedFooter(currentPage, lastPage); + + var msg = await channel.EmbedAsync(embed) as IUserMessage; + + if (currentPage >= lastPage && lastPage == 1) + return; + + await msg.AddReactionAsync(arrow_left).ConfigureAwait(false); + await msg.AddReactionAsync(arrow_right).ConfigureAwait(false); + + await Task.Delay(2000).ConfigureAwait(false); + + Action changePage = async r => + { + try + { + if (r.Emoji.Name == arrow_left) + { + if (currentPage == 1) + return; + await msg.ModifyAsync(x => x.Embed = pageFunc(--currentPage).AddPaginatedFooter(currentPage, lastPage).Build()).ConfigureAwait(false); + } + else if (r.Emoji.Name == arrow_right) + { + if (lastPage == null || lastPage > currentPage) + await msg.ModifyAsync(x => x.Embed = pageFunc(++currentPage).AddPaginatedFooter(currentPage, lastPage).Build()).ConfigureAwait(false); + } + } + catch (Exception ex) { Console.WriteLine(ex); } + }; + + using (msg.OnReaction(changePage, changePage)) + { + await Task.Delay(30000).ConfigureAwait(false); + } + + await msg.RemoveAllReactionsAsync().ConfigureAwait(false); + } + + private static EmbedBuilder AddPaginatedFooter(this EmbedBuilder embed, int curPage, int? lastPage) + { + if (lastPage != null) + return embed.WithFooter(efb => efb.WithText($"page {curPage} / {lastPage}")); + else + return embed.WithFooter(efb => efb.WithText($"page {curPage}")); + } + + public static ReactionEventWrapper OnReaction(this IUserMessage msg, Action reactionAdded, Action reactionRemoved = null) + { + if (reactionRemoved == null) + reactionRemoved = delegate { }; + + var wrap = new ReactionEventWrapper(msg); + wrap.OnReactionAdded += reactionAdded; + wrap.OnReactionRemoved += reactionRemoved; + return wrap; + } + public static void AddFakeHeaders(this HttpClient http) { http.DefaultRequestHeaders.Clear(); @@ -44,7 +113,8 @@ namespace NadekoBot.Extensions public static string GetPrefix(this ModuleInfo module) => NadekoBot.ModulePrefixes[module.GetTopLevelModule().Name]; - public static ModuleInfo GetTopLevelModule(this ModuleInfo module) { + public static ModuleInfo GetTopLevelModule(this ModuleInfo module) + { while (module.Parent != null) { module = module.Parent; @@ -93,7 +163,7 @@ namespace NadekoBot.Extensions public static bool IsInteger(this decimal number) => number == Math.Truncate(number); - public static string SanitizeMentions(this string str) => + public static string SanitizeMentions(this string str) => str.Replace("@everyone", "@everyοne").Replace("@here", "@һere"); public static double UnixTimestamp(this DateTime dt) => dt.ToUniversalTime().Subtract(new DateTime(1970, 1, 1)).TotalSeconds; @@ -105,7 +175,7 @@ namespace NadekoBot.Extensions => await (await user.CreateDMChannelAsync()).SendMessageAsync("", embed: new EmbedBuilder().WithColor(NadekoBot.OkColor).WithDescription(text)); public static async Task SendConfirmAsync(this IUser user, string title, string text, string url = null) - => await(await user.CreateDMChannelAsync()).SendMessageAsync("", embed: new EmbedBuilder().WithColor(NadekoBot.OkColor).WithDescription(text) + => await (await user.CreateDMChannelAsync()).SendMessageAsync("", embed: new EmbedBuilder().WithColor(NadekoBot.OkColor).WithDescription(text) .WithTitle(title).WithUrl(url)); public static async Task SendErrorAsync(this IUser user, string title, string error, string url = null) @@ -126,7 +196,7 @@ namespace NadekoBot.Extensions public static IEnumerable Members(this IRole role) => role.Guild.GetUsersAsync().GetAwaiter().GetResult().Where(u => u.RoleIds.Contains(role.Id)) ?? Enumerable.Empty(); - + public static Task EmbedAsync(this IMessageChannel ch, EmbedBuilder embed, string msg = "") => ch.SendMessageAsync(msg, embed: embed); @@ -153,7 +223,7 @@ namespace NadekoBot.Extensions ```"); } - public static Task SendTableAsync(this IMessageChannel ch, IEnumerable items, Func howToPrint, int columns = 3) => + public static Task SendTableAsync(this IMessageChannel ch, IEnumerable items, Func howToPrint, int columns = 3) => ch.SendTableAsync("", items, howToPrint, columns); /// @@ -286,7 +356,7 @@ namespace NadekoBot.Extensions } - public static string ToJson(this T any, Formatting formatting = Formatting.Indented) => + public static string ToJson(this T any, Formatting formatting = Formatting.Indented) => JsonConvert.SerializeObject(any, formatting); public static int KiB(this int value) => value * 1024; @@ -317,7 +387,7 @@ namespace NadekoBot.Extensions var canvasPixels = canvas.Lock(); int offsetX = 0; - foreach (var img in imgList.Select(img=>img.Lock())) + foreach (var img in imgList.Select(img => img.Lock())) { for (int i = 0; i < img.Width; i++) { @@ -326,7 +396,7 @@ namespace NadekoBot.Extensions canvasPixels[i + offsetX, j] = img[i, j]; } } - offsetX += img.Width; + offsetX += img.Width; } return canvas; @@ -345,4 +415,4 @@ namespace NadekoBot.Extensions public static bool IsDiscordInvite(this string str) => filterRegex.IsMatch(str); } -} +} \ No newline at end of file