Browse Source

Merge branch 'dev' into webhook-example

pull/1241/head
Christopher F GitHub 6 years ago
parent
commit
52b8beeec2
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
100 changed files with 1361 additions and 520 deletions
  1. +17
    -0
      Discord.Net.sln
  2. +1
    -1
      Discord.Net.targets
  3. +1
    -1
      LICENSE
  4. +2
    -2
      README.md
  5. +0
    -94
      appveyor.yml
  6. +31
    -0
      azure-pipelines.yml
  7. +17
    -0
      azure/build.yml
  8. +32
    -0
      azure/deploy.yml
  9. +35
    -10
      docs/CONTRIBUTING.md
  10. +2
    -2
      docs/_overwrites/Commands/PreconditionAttribute.Remarks.Inclusion.md
  11. +5
    -4
      docs/_overwrites/Common/EmbedBuilder.Overwrites.md
  12. BIN
      docs/_template/last-modified/plugins/LastModifiedPostProcessor.dll
  13. BIN
      docs/_template/last-modified/plugins/LibGit2Sharp.dll
  14. +2
    -2
      docs/_template/last-modified/plugins/LibGit2Sharp.dll.config
  15. BIN
      docs/_template/last-modified/plugins/lib/alpine-x64/libgit2-8e0b172.so
  16. BIN
      docs/_template/last-modified/plugins/lib/alpine-x64/libgit2-a904fc6.so
  17. BIN
      docs/_template/last-modified/plugins/lib/debian.9-x64/libgit2-8e0b172.so
  18. BIN
      docs/_template/last-modified/plugins/lib/debian.9-x64/libgit2-a904fc6.so
  19. BIN
      docs/_template/last-modified/plugins/lib/fedora-x64/libgit2-8e0b172.so
  20. BIN
      docs/_template/last-modified/plugins/lib/fedora-x64/libgit2-a904fc6.so
  21. BIN
      docs/_template/last-modified/plugins/lib/linux-x64/libgit2-8e0b172.so
  22. BIN
      docs/_template/last-modified/plugins/lib/linux-x64/libgit2-a904fc6.so
  23. BIN
      docs/_template/last-modified/plugins/lib/osx/libgit2-8e0b172.dylib
  24. BIN
      docs/_template/last-modified/plugins/lib/osx/libgit2-a904fc6.dylib
  25. BIN
      docs/_template/last-modified/plugins/lib/rhel-x64/libgit2-8e0b172.so
  26. BIN
      docs/_template/last-modified/plugins/lib/rhel-x64/libgit2-a904fc6.so
  27. BIN
      docs/_template/last-modified/plugins/lib/ubuntu.18.04-x64/libgit2-a904fc6.so
  28. BIN
      docs/_template/last-modified/plugins/lib/win32/x64/git2-8e0b172.dll
  29. BIN
      docs/_template/last-modified/plugins/lib/win32/x64/git2-a904fc6.dll
  30. BIN
      docs/_template/last-modified/plugins/lib/win32/x86/git2-8e0b172.dll
  31. BIN
      docs/_template/last-modified/plugins/lib/win32/x86/git2-a904fc6.dll
  32. +21
    -0
      docs/_template/light-dark-theme/docfx-material-license.md
  33. +1
    -0
      docs/_template/light-dark-theme/partials/head.tmpl.partial
  34. +15
    -1
      docs/_template/light-dark-theme/styles/dark.css
  35. +0
    -1
      docs/_template/light-dark-theme/styles/docfx.vendor.minify.css
  36. +13
    -4
      docs/_template/light-dark-theme/styles/gray.css
  37. +1
    -1
      docs/_template/light-dark-theme/styles/light.css
  38. +68
    -12
      docs/_template/light-dark-theme/styles/master.css
  39. +199
    -0
      docs/_template/light-dark-theme/styles/material.css
  40. +1
    -1
      docs/docfx.json
  41. +3
    -0
      docs/filterConfig.yml
  42. +3
    -0
      docs/guides/commands/samples/dependency-injection/dependency_map_setup.cs
  43. +2
    -2
      docs/guides/commands/samples/dependency-injection/dependency_module.cs
  44. +2
    -11
      docs/guides/commands/samples/intro/command_handler.cs
  45. +0
    -1
      docs/guides/commands/samples/post-execution/command_exception_log.cs
  46. +3
    -2
      docs/guides/commands/samples/typereaders/typereader.cs
  47. +0
    -4
      docs/guides/concepts/connections.md
  48. +1
    -1
      docs/guides/concepts/entities.md
  49. +0
    -23
      docs/guides/concepts/samples/connections.cs
  50. +14
    -19
      docs/guides/concepts/samples/logging.cs
  51. +30
    -68
      docs/guides/getting_started/first-bot.md
  52. +33
    -25
      docs/guides/getting_started/installing.md
  53. +14
    -3
      docs/guides/getting_started/nightlies.md
  54. +0
    -10
      docs/guides/getting_started/samples/first-bot/complete.cs
  55. +3
    -6
      docs/guides/introduction/intro.md
  56. +1
    -0
      samples/02_commands_framework/Modules/PublicModule.cs
  57. +3
    -1
      samples/02_commands_framework/Program.cs
  58. +15
    -3
      samples/02_commands_framework/Services/CommandHandlingService.cs
  59. +5
    -0
      src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs
  60. +1
    -0
      src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs
  61. +1
    -0
      src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs
  62. +1
    -0
      src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs
  63. +5
    -0
      src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs
  64. +2
    -9
      src/Discord.Net.Commands/CommandService.cs
  65. +3
    -3
      src/Discord.Net.Core/Discord.Net.Core.csproj
  66. +3
    -1
      src/Discord.Net.Core/Entities/Channels/ChannelType.cs
  67. +8
    -36
      src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs
  68. +37
    -48
      src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs
  69. +1
    -1
      src/Discord.Net.Core/Entities/Channels/ITextChannel.cs
  70. +2
    -10
      src/Discord.Net.Core/Entities/Guilds/IGuild.cs
  71. +5
    -1
      src/Discord.Net.Core/Entities/Messages/MessageType.cs
  72. +9
    -20
      src/Discord.Net.Core/Entities/Users/IUser.cs
  73. +15
    -0
      src/Discord.Net.Core/Extensions/AttachmentExtensions.cs
  74. +0
    -4
      src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs
  75. +3
    -1
      src/Discord.Net.Core/Format.cs
  76. +4
    -4
      src/Discord.Net.Core/IDiscordClient.cs
  77. +66
    -1
      src/Discord.Net.Core/Utils/TokenUtils.cs
  78. +46
    -0
      src/Discord.Net.Examples/Core/Entities/Channels/IGuildChannel.Examples.cs
  79. +114
    -0
      src/Discord.Net.Examples/Core/Entities/Channels/IMessageChannel.Examples.cs
  80. +25
    -0
      src/Discord.Net.Examples/Core/Entities/Guilds/IGuild.Examples.cs
  81. +38
    -0
      src/Discord.Net.Examples/Core/Entities/Users/IUser.Examples.cs
  82. +21
    -0
      src/Discord.Net.Examples/Discord.Net.Examples.csproj
  83. +117
    -0
      src/Discord.Net.Examples/WebSocket/BaseSocketClient.Events.Examples.cs
  84. +7
    -1
      src/Discord.Net.Rest/API/Rest/UploadFileParams.cs
  85. +7
    -1
      src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs
  86. +11
    -3
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs
  87. +14
    -5
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookCreateAuditLogData.cs
  88. +4
    -4
      src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs
  89. +3
    -3
      src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs
  90. +1
    -0
      src/Discord.Net.Rest/Entities/Channels/RestChannel.cs
  91. +8
    -8
      src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs
  92. +8
    -8
      src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs
  93. +9
    -7
      src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs
  94. +53
    -0
      src/Discord.Net.Rest/Entities/Channels/RestNewsChannel.cs
  95. +9
    -9
      src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs
  96. +1
    -1
      src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs
  97. +4
    -4
      src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs
  98. +2
    -2
      src/Discord.Net.WebSocket/API/Gateway/MessageDeleteBulkEvent.cs
  99. +83
    -0
      src/Discord.Net.WebSocket/BaseSocketClient.Events.cs
  100. +14
    -10
      src/Discord.Net.WebSocket/BaseSocketClient.cs

+ 17
- 0
Discord.Net.sln View File

@@ -34,6 +34,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "03_sharded_client", "sample
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "04_webhook_client", "samples\04_webhook_client\04_webhook_client.csproj", "{88B77A5B-0BC0-4E99-8FD9-D83F6999F562}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{D1F0271E-0EE2-4B66-AC3D-9871B7E1C4CF}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Examples", "src\Discord.Net.Examples\Discord.Net.Examples.csproj", "{7EA96B2B-4D71-458D-9423-839362DC38BE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -188,6 +192,18 @@ Global
{88B77A5B-0BC0-4E99-8FD9-D83F6999F562}.Release|x64.Build.0 = Release|Any CPU
{88B77A5B-0BC0-4E99-8FD9-D83F6999F562}.Release|x86.ActiveCfg = Release|Any CPU
{88B77A5B-0BC0-4E99-8FD9-D83F6999F562}.Release|x86.Build.0 = Release|Any CPU
{7EA96B2B-4D71-458D-9423-839362DC38BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7EA96B2B-4D71-458D-9423-839362DC38BE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7EA96B2B-4D71-458D-9423-839362DC38BE}.Debug|x64.ActiveCfg = Debug|Any CPU
{7EA96B2B-4D71-458D-9423-839362DC38BE}.Debug|x64.Build.0 = Debug|Any CPU
{7EA96B2B-4D71-458D-9423-839362DC38BE}.Debug|x86.ActiveCfg = Debug|Any CPU
{7EA96B2B-4D71-458D-9423-839362DC38BE}.Debug|x86.Build.0 = Debug|Any CPU
{7EA96B2B-4D71-458D-9423-839362DC38BE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7EA96B2B-4D71-458D-9423-839362DC38BE}.Release|Any CPU.Build.0 = Release|Any CPU
{7EA96B2B-4D71-458D-9423-839362DC38BE}.Release|x64.ActiveCfg = Release|Any CPU
{7EA96B2B-4D71-458D-9423-839362DC38BE}.Release|x64.Build.0 = Release|Any CPU
{7EA96B2B-4D71-458D-9423-839362DC38BE}.Release|x86.ActiveCfg = Release|Any CPU
{7EA96B2B-4D71-458D-9423-839362DC38BE}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -203,6 +219,7 @@ Global
{4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}
{9B4C4AFB-3D15-44C6-9E36-12ED625AAA26} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}
{88B77A5B-0BC0-4E99-8FD9-D83F6999F562} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}
{7EA96B2B-4D71-458D-9423-839362DC38BE} = {D1F0271E-0EE2-4B66-AC3D-9871B7E1C4CF}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D2404771-EEC8-45F2-9D71-F3373F6C1495}


+ 1
- 1
Discord.Net.targets View File

@@ -1,6 +1,6 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VersionPrefix>2.0.2</VersionPrefix>
<VersionPrefix>2.1.0</VersionPrefix>
<VersionSuffix>dev</VersionSuffix>
<Authors>RogueException</Authors>
<PackageTags>discord;discordapp</PackageTags>


+ 1
- 1
LICENSE View File

@@ -1,6 +1,6 @@
The MIT License (MIT)

Copyright (c) 2015-2017 Discord.Net Contributors
Copyright (c) 2015-2019 Discord.Net Contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal


+ 2
- 2
README.md View File

@@ -1,12 +1,12 @@
# Discord.Net
[![NuGet](https://img.shields.io/nuget/vpre/Discord.Net.svg?maxAge=2592000?style=plastic)](https://www.nuget.org/packages/Discord.Net)
[![MyGet](https://img.shields.io/myget/discord-net/vpre/Discord.Net.svg)](https://www.myget.org/feed/Packages/discord-net)
[![Build status](https://ci.appveyor.com/api/projects/status/5sb7n8a09w9clute/branch/dev?svg=true)](https://ci.appveyor.com/project/RogueException/discord-net/branch/dev)
[![Build Status](https://dev.azure.com/discord-net/Discord.Net/_apis/build/status/discord-net.Discord.Net?branchName=dev)](https://dev.azure.com/discord-net/Discord.Net/_build/latest?definitionId=1&branchName=dev)
[![Discord](https://discordapp.com/api/guilds/81384788765712384/widget.png)](https://discord.gg/jkrBmQR)

An unofficial .NET API Wrapper for the Discord client (http://discordapp.com).

Check out the [documentation](https://discord.foxbot.me/docs/) or join the [Discord API Chat](https://discord.gg/jkrBmQR).
Check out the [documentation](https://discord.foxbot.me/) or join the [Discord API Chat](https://discord.gg/jkrBmQR).

## Installation
### Stable (NuGet)


+ 0
- 94
appveyor.yml View File

@@ -1,94 +0,0 @@
version: build-{build}
branches:
only:
- dev
image:
- Visual Studio 2017
- Ubuntu

nuget:
disable_publish_on_pr: true
pull_requests:
do_not_increment_build_number: true
# Use the default clone_folder
# Windows: C:\Projects\discord-net
# Ubuntu: /home/appveyor/projects/discord-net
cache: test/Discord.Net.Tests/cache.db

environment:
DOTNET_CLI_TELEMETRY_OPTOUT: 1
DNET_TEST_TOKEN:
secure: l7h5e7UE7yRd70hAB97kjPiQpPOShwqoBbOzEAYQ+XBd/Pre5OA33IXa3uisdUeQJP/nPFhcOsI+yn7WpuFaoQ==
DNET_TEST_GUILDID: 273160668180381696
init:
- ps: $Env:BUILD = "$($Env:APPVEYOR_BUILD_NUMBER.PadLeft(5, "0"))"

build_script:
- ps: >-
if ($isLinux)
{
# AppVeyor Linux images do not have appveyor-retry, which retries the commands a few times
# until the command exits with code 0.
# So, this is done with a short script.
$code = 0
$counter = 0
do
{
dotnet restore Discord.Net.sln -v Minimal /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG"
$code = $LASTEXITCODE
$counter++
if ($code -ne 0)
{
# Wait 5s before attempting to run again
Start-sleep -Seconds 5
}
}
until ($counter -eq 5 -or $code -eq 0)
}
else
{
appveyor-retry dotnet restore Discord.Net.sln -v Minimal /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG"
}
- ps: dotnet build Discord.Net.sln -c "Release" /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG"
after_build:
- ps: if ($isWindows) { dotnet pack "src\Discord.Net.Core\Discord.Net.Core.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" }
- ps: if ($isWindows) { dotnet pack "src\Discord.Net.Rest\Discord.Net.Rest.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" }
- ps: if ($isWindows) { dotnet pack "src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" }
- ps: if ($isWindows) { dotnet pack "src\Discord.Net.Commands\Discord.Net.Commands.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" }
- ps: if ($isWindows) { dotnet pack "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" }
- ps: if ($isWindows) { dotnet pack "src\Discord.Net.Providers.WS4Net\Discord.Net.Providers.WS4Net.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" }
- ps: if ($isWindows) { dotnet pack "src\Discord.Net.Analyzers\Discord.Net.Analyzers.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" }
- ps: >-
if ($isWindows)
{
if ($Env:APPVEYOR_REPO_TAG -eq "true") {
nuget pack src/Discord.Net/Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix=""
} else {
nuget pack src/Discord.Net/Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="-$Env:BUILD"
}
}
- ps: if ($isWindows) { Get-ChildItem artifacts/*.nupkg | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } }

test_script:
- ps: >-
if ($APPVEYOR_PULL_REQUEST_NUMBER -eq "") {
dotnet test test/Discord.Net.Tests/Discord.Net.Tests.csproj -c "Release" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG"
}

deploy:
- provider: NuGet
server: https://www.myget.org/F/discord-net/api/v2/package
api_key:
secure: Jl7BXeUjRnkVHDMBuUWSXcEOkrli1PBleW2IiLyUs5j63UNUNp1hcjaUJRujx9lz
symbol_server: https://www.myget.org/F/discord-net/symbols/api/v2/package
on:
branch: dev
CI_WINDOWS: true
- provider: NuGet
server: https://www.myget.org/F/rogueexception/api/v2/package
api_key:
secure: D+vW2O2LBf/iJb4f+q8fkyIW2VdIYIGxSYLWNrOD4BHlDBZQlJipDbNarWjUr2Kn
symbol_server: https://www.myget.org/F/rogueexception/symbols/api/v2/package
on:
branch: dev
CI_WINDOWS: true

+ 31
- 0
azure-pipelines.yml View File

@@ -0,0 +1,31 @@
variables:
buildConfiguration: Release
buildTag: $[ startsWith(variables['Build.SourceBranch'], 'refs/tags') ]
buildNumber: $[ variables['Build.BuildNumber'] ]


jobs:
- job: Linux
pool:
vmImage: 'ubuntu-16.04'
steps:
- template: azure/build.yml

- job: Windows_build
pool:
vmImage: 'vs2017-win2016'
condition: ne(variables['Build.SourceBranch'], 'refs/heads/dev')
steps:
- template: azure/build.yml

- job: Windows_deploy
pool:
vmImage: 'vs2017-win2016'
condition: |
and (
succeeded(),
eq(variables['Build.SourceBranch'], 'refs/heads/dev')
)
steps:
- template: azure/build.yml
- template: azure/deploy.yml

+ 17
- 0
azure/build.yml View File

@@ -0,0 +1,17 @@
steps:
- script: dotnet restore -v minimal Discord.Net.sln
displayName: Restore packages

- script: dotnet build "Discord.Net.sln" --no-restore -v minimal -c $(buildConfiguration) /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag)
displayName: Build projects

- script: dotnet test "test/Discord.Net.Tests/Discord.Net.Tests.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) --logger trx
# TODO: update this to support multiple tests
displayName: Test projects

- task: PublishTestResults@2
displayName: Publish test results
condition: succeededOrFailed()
inputs:
testRunner: VSTest
testResultsFiles: '**/*.trx'

+ 32
- 0
azure/deploy.yml View File

@@ -0,0 +1,32 @@
steps:
- script: |
dotnet pack "src\Discord.Net.Core\Discord.Net.Core.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "../../artifacts/" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag)
dotnet pack "src\Discord.Net.Rest\Discord.Net.Rest.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "../../artifacts/" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag)
dotnet pack "src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "../../artifacts/" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag)
dotnet pack "src\Discord.Net.Commands\Discord.Net.Commands.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "../../artifacts/" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag)
dotnet pack "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "../../artifacts/" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag)
dotnet pack "src\Discord.Net.Providers.WS4Net\Discord.Net.Providers.WS4Net.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "../../artifacts/" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag)
dotnet pack "src\Discord.Net.Analyzers\Discord.Net.Analyzers.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "../../artifacts/" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag)
displayName: Pack projects

- task: NuGet@0
inputs:
command: pack
arguments: src/Discord.Net/Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix=""
displayName: Pack metapackage (release mode)
condition: eq(variables['buildTag'], True)

- task: NuGet@0
inputs:
command: pack
arguments: src/Discord.Net/Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="-$(buildNumber)"
displayName: Pack metapackage
condition: eq(variables['buildTag'], False)

- task: NuGetCommand@2
displayName: Push to NuGet
inputs:
command: push
nuGetFeedType: external
packagesToPush: 'artifacts/*.nupkg'
publishFeedCredentials: myget-discord

+ 35
- 10
docs/CONTRIBUTING.md View File

@@ -1,23 +1,46 @@
# Contributing to Docs

## General Guidelines
First of all, thank you for your interest in contributing to our
documentation work. We really appreciate it! That being said,
there are several guidelines you should attempt to follow when adding
to/changing the documentation.

We do not have any strict conditions for writing documentation,
but keep these guidelines in mind:
## General Guidelines

* Keep code samples in the `guides/samples` folder
* Keep code samples in each section's `samples` folder
* When referencing an object in the API, link to its page in the
API documentation
* Documentation should be written in an FAQ/Wiki-style format
* Documentation should be written in clear and proper English*

\* If anyone is interested in translating documentation into other
languages, please open an issue or contact me on
Discord (`foxbot#0282`).
languages, please open an issue or contact `foxbot#0282` on
Discord.

## XML Docstrings Guidelines

## Style Consistencies
* When using the `<summary>` tag, use concise verbs. For example:

```cs
/// <summary> Gets or sets the guild user in this object. </summary>
public IGuildUser GuildUser { get; set; }
```

* Use a ruler set at 70 characters
* The `<summary>` tag should not be more than 3 lines long. Consider
simplifying the terms or using the `<remarks>` tag instead.
* When using the `<code>` tag, put the code sample within the
`src/Discord.Net.Examples` project under the corresponding path of
the object and surround the code with a `#region` tag.
* If the remarks you are looking to write are too long, consider
writing a shorter version in the XML docs while keeping the longer
version in the `overwrites` folder using the DocFX overwrites syntax.
* You may find an example of this in the samples provided within
the folder.

## Docs Guide Guidelines

* Use a ruler set at 70 characters (use the docs workspace provided
if you are using Visual Studio Code)
* Links should use long syntax
* Pages should be short and concise, not broad and long

@@ -31,5 +54,7 @@ Please consult the [API Documentation] for more information.

## Recommended Reads

* http://docs.microsoft.com
* http://flask.pocoo.org/docs/0.12/
* [Microsoft Docs](https://docs.microsoft.com)
* [Flask Docs](https://flask.pocoo.org/docs/1.0/)
* [DocFX Manual](https://dotnet.github.io/docfx/)
* [Sandcastle XML Guide](http://ewsoftware.github.io/XMLCommentsGuide)

+ 2
- 2
docs/_overwrites/Commands/PreconditionAttribute.Remarks.Inclusion.md View File

@@ -1,6 +1,6 @@
A "precondidtion" in the command system is used to determine if a
A "precondition" in the command system is used to determine if a
condition is met before entering the command task. Using a
precondidtion may aid in keeping a well-organized command logic.
precondition may aid in keeping a well-organized command logic.

The most common use case being whether a user has sufficient
permission to execute the command.

+ 5
- 4
docs/_overwrites/Common/EmbedBuilder.Overwrites.md View File

@@ -40,9 +40,10 @@ public async Task SendRichEmbedAsync()
.WithTitle("I overwrote \"Hello world!\"")
.WithDescription("I am a description.")
.WithUrl("https://example.com")
.WithCurrentTimestamp()
.Build();
await ReplyAsync(embed: embed);
.WithCurrentTimestamp();
//Your embed needs to be built before it is able to be sent
await ReplyAsync(embed: embed.Build());
}
```

@@ -65,4 +66,4 @@ public async Task SendEmbedWithImageAsync()
}.Build();
await Context.Channel.SendFileAsync(fileName, embed: embed);
}
```
```

BIN
docs/_template/last-modified/plugins/LastModifiedPostProcessor.dll View File


BIN
docs/_template/last-modified/plugins/LibGit2Sharp.dll View File


+ 2
- 2
docs/_template/last-modified/plugins/LibGit2Sharp.dll.config View File

@@ -1,4 +1,4 @@
<configuration>
<dllmap os="linux" cpu="x86-64" wordsize="64" dll="git2-8e0b172" target="lib/linux-x64/libgit2-8e0b172.so" />
<dllmap os="osx" cpu="x86,x86-64" dll="git2-8e0b172" target="lib/osx/libgit2-8e0b172.dylib" />
<dllmap os="linux" cpu="x86-64" wordsize="64" dll="git2-a904fc6" target="lib/linux-x64/libgit2-a904fc6.so" />
<dllmap os="osx" cpu="x86,x86-64" dll="git2-a904fc6" target="lib/osx/libgit2-a904fc6.dylib" />
</configuration>

BIN
docs/_template/last-modified/plugins/lib/alpine-x64/libgit2-8e0b172.so View File


BIN
docs/_template/last-modified/plugins/lib/alpine-x64/libgit2-a904fc6.so View File


BIN
docs/_template/last-modified/plugins/lib/debian.9-x64/libgit2-8e0b172.so View File


BIN
docs/_template/last-modified/plugins/lib/debian.9-x64/libgit2-a904fc6.so View File


BIN
docs/_template/last-modified/plugins/lib/fedora-x64/libgit2-8e0b172.so View File


BIN
docs/_template/last-modified/plugins/lib/fedora-x64/libgit2-a904fc6.so View File


BIN
docs/_template/last-modified/plugins/lib/linux-x64/libgit2-8e0b172.so View File


BIN
docs/_template/last-modified/plugins/lib/linux-x64/libgit2-a904fc6.so View File


BIN
docs/_template/last-modified/plugins/lib/osx/libgit2-8e0b172.dylib View File


BIN
docs/_template/last-modified/plugins/lib/osx/libgit2-a904fc6.dylib View File


BIN
docs/_template/last-modified/plugins/lib/rhel-x64/libgit2-8e0b172.so View File


BIN
docs/_template/last-modified/plugins/lib/rhel-x64/libgit2-a904fc6.so View File


BIN
docs/_template/last-modified/plugins/lib/ubuntu.18.04-x64/libgit2-a904fc6.so View File


BIN
docs/_template/last-modified/plugins/lib/win32/x64/git2-8e0b172.dll View File


BIN
docs/_template/last-modified/plugins/lib/win32/x64/git2-a904fc6.dll View File


BIN
docs/_template/last-modified/plugins/lib/win32/x86/git2-8e0b172.dll View File


BIN
docs/_template/last-modified/plugins/lib/win32/x86/git2-a904fc6.dll View File


+ 21
- 0
docs/_template/light-dark-theme/docfx-material-license.md View File

@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2019 Oscar Vásquez

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

+ 1
- 0
docs/_template/light-dark-theme/partials/head.tmpl.partial View File

@@ -15,6 +15,7 @@
<link rel="stylesheet" href="{{_rel}}styles/docfx.css">
<link rel="stylesheet" href="{{_rel}}styles/master.css">
<link rel="stylesheet" href="{{_rel}}styles/main.css">
<link rel="stylesheet" href="{{_rel}}styles/material.css">
<link rel="stylesheet" href="{{_rel}}styles/theme-switcher.css">
<link href="https://cdn.rawgit.com/noelboss/featherlight/1.7.6/release/featherlight.min.css" type="text/css" rel="stylesheet" />
<meta name="theme-color" content="#99AAB5"/>


+ 15
- 1
docs/_template/light-dark-theme/styles/dark.css View File

@@ -7,6 +7,15 @@ body {
color: #C0C0C0;
}

h1,
h2,
h3,
h4,
h5,
h6 {
color: #E0E0E0;
}

button,
a {
color: #64B5F6;
@@ -258,6 +267,11 @@ tbody>tr {
border-top: 2px solid rgb(173, 173, 173)
}

/* top navbar */
.navbar-inverse[role="navigation"] {
background-color: #2C2F33;
}

/* select */

select {
@@ -304,5 +318,5 @@ span.arrow-r{
}

.logo-switcher {
background: url("/marketing/logo/SVG/Combinationmark White.svg") no-repeat;
background: url("../marketing/logo/SVG/Combinationmark White.svg") no-repeat;
}

+ 0
- 1
docs/_template/light-dark-theme/styles/docfx.vendor.minify.css View File

@@ -361,7 +361,6 @@ pre {
word-break: break-all;
word-wrap: break-word;
color: #333;
border: 1px solid #ccc;
border-radius: 4px;
}



+ 13
- 4
docs/_template/light-dark-theme/styles/gray.css View File

@@ -7,6 +7,15 @@ body {
color: #dddddd;
}

h1,
h2,
h3,
h4,
h5,
h6 {
color: #EEEEEE;
}

button,
a {
color: #64B5F6;
@@ -39,13 +48,13 @@ hr {
}

/* top navbar */
.navbar-inverse[role="navigation"] {
/*.navbar-inverse[role="navigation"] {
background-color: #2C2F33;
}
}*/

/* sub navbar (below top) */
.subnav {
background: #282B2F
background: rgb(69, 75, 82)
}


@@ -311,5 +320,5 @@ span.arrow-r{
}

.logo-switcher {
background: url("/marketing/logo/SVG/Combinationmark White.svg") no-repeat;
background: url("../marketing/logo/SVG/Combinationmark White.svg") no-repeat;
}

+ 1
- 1
docs/_template/light-dark-theme/styles/light.css View File

@@ -113,5 +113,5 @@ span.arrow-r{
}

.logo-switcher {
background: url("/marketing/logo/SVG/Combinationmark.svg") no-repeat;
background: url("../marketing/logo/SVG/Combinationmark.svg") no-repeat;
}

+ 68
- 12
docs/_template/light-dark-theme/styles/master.css View File

@@ -1,14 +1,37 @@
@import url('https://fonts.googleapis.com/css?family=Titillium+Web|Noto+Sans');
@import url('https://fonts.googleapis.com/css?family=Roboto|Muli|Fira+Mono');

html,
body {
font-family: 'Titillium Web', 'Segoe UI', Tahoma, Helvetica, sans-serif;
font-family: 'Roboto', 'Segoe UI', Tahoma, Helvetica, sans-serif;
font-display: optional;
height: 100%;
font-size: 15px;
scroll-behavior: smooth;
}

code{
font-family: 'Fira Mono', 'Courier New', Courier, monospace
}

h1,
h2,
h3,
h4,
h5,
h6 {
font-family: 'Muli', Verdana, Geneva, Tahoma, sans-serif;
line-height: 130%;
}

h1,
.h1,
h2,
.h2,
h3,
.h3 {
font-weight: 600;
}

#logo
{
max-width: 100px;
@@ -25,6 +48,14 @@ li,
line-height: 160%;
}

.toc-filter{
background: inherit !important;
}

.affix ul>li.active>ul, .affix ul>li.active>a:before, .affix ul>li>a:hover:before{
white-space: normal;
}

img {
box-shadow: 0px 0px 3px 0px rgb(66, 66, 66);
max-width: 95% !important;
@@ -57,16 +88,6 @@ article.content h6{
transition: all .25s ease-in-out;
}

h1,
h2,
h3,
h4,
h5,
h6 {
font-family: 'Noto Sans', Verdana, Geneva, Tahoma, sans-serif;
line-height: 130%;
}

.sideaffix {
line-height: 140%;
}
@@ -173,3 +194,38 @@ span.arrow-d{
span.arrow-r{
top: 6px; position: relative;
}

/* widen viewport */

@media (min-width: 1085px) {
.container {
width: calc(100% - 15vw);
max-width: calc(100% - 15vw);
}
}

/* fix level indentation */

.level2 {
padding: 0 5px;
}

.level3 {
padding: 0 5px;
font-size: 90%;
}

.level4 {
padding: 0 5px;
font-size: 85%;
}

.level5 {
padding: 0 5px;
font-size: 80%;
}

.level6 {
padding: 0 5px;
font-size: 75%;
}

+ 199
- 0
docs/_template/light-dark-theme/styles/material.css View File

@@ -0,0 +1,199 @@
body {
color: #34393e;
line-height: 1.5;
/*font-size: 16px;*/
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
word-wrap: break-word
}

/* HEADINGS */

h1 {
font-weight: 600;
font-size: 32px;
}

h2 {
font-weight: 600;
font-size: 24px;
line-height: 1.8;
}

h3 {
font-weight: 600;
font-size: 20px;
line-height: 1.8;
}

h5 {
font-size: 14px;
padding: 10px 0px;
}

article h1,
article h2,
article h3,
article h4 {
margin-top: 35px;
margin-bottom: 15px;
}

article h4 {
padding-bottom: 8px;
border-bottom: 2px solid #ddd;
}

/* NAVBAR */

.navbar-brand>img {
color: #fff;
}

.navbar {
border: none;
/* Both navbars use box-shadow */
-webkit-box-shadow: 0px 1px 3px 0px rgba(100, 100, 100, 0.5);
-moz-box-shadow: 0px 1px 3px 0px rgba(100, 100, 100, 0.5);
box-shadow: 0px 1px 3px 0px rgba(100, 100, 100, 0.5);
}

.subnav {
border-top: 1px solid #ddd;
background-color: #fff;
}

.navbar-inverse {
background-color: #0d47a1;
z-index: 100;
}

.navbar-inverse .navbar-nav>li>a,
.navbar-inverse .navbar-text {
color: #fff;
/*background-color: #0d47a1;*/
border-bottom: 3px solid transparent;
padding-bottom: 12px;
}

.navbar-inverse .navbar-nav>li>a:focus,
.navbar-inverse .navbar-nav>li>a:hover {
color: #fff;
background-color: #1157c0;
border-bottom: 3px solid white;
}

.navbar-inverse .navbar-nav>.active>a,
.navbar-inverse .navbar-nav>.active>a:focus,
.navbar-inverse .navbar-nav>.active>a:hover {
color: #fff;
background-color: #1157c0;
border-bottom: 3px solid white;
}

.navbar-form .form-control {
border: none;
border-radius: 20px;
}

/* SIDEBAR */

/*.toc .level1>li {
font-weight: 400;
}*/

.toc .nav>li>a {
color: #34393e;
}

.sidefilter {
background-color: #fff;
border-left: none;
border-right: none;
}

.sidefilter {
background-color: #fff;
border-left: none;
border-right: none;
}

.toc-filter {
padding: 10px;
margin: 0;
}

.toc-filter>input {
border: 2px solid #ddd;
border-radius: 20px;
}

.toc-filter>.filter-icon {
display: none;
}

.sidetoc>.toc {
overflow-x: hidden;
}

.sidetoc {
border: none;
}

/* ALERTS */

.alert {
padding: 0px 0px 5px 0px;
color: inherit;
background-color: inherit;
border: none;
box-shadow: 0px 2px 2px 0px rgba(100, 100, 100, 0.4);
}

.alert>p {
margin-bottom: 0;
padding: 5px 10px;
}

.alert>ul {
margin-bottom: 0;
padding: 5px 40px;
}

.alert>h5 {
padding: 10px 15px;
margin-top: 0;
text-transform: uppercase;
font-weight: bold;
border-radius: 4px 4px 0 0;
}

.alert-info>h5 {
color: #1976d2;
border-bottom: 4px solid #1976d2;
background-color: #e3f2fd;
}

.alert-warning>h5 {
color: #f57f17;
border-bottom: 4px solid #f57f17;
background-color: #fff3e0;
}

.alert-danger>h5 {
color: #d32f2f;
border-bottom: 4px solid #d32f2f;
background-color: #ffebee;
}

/* CODE HIGHLIGHT */
pre {
padding: 9.5px;
margin: 10px 10px 10px;
font-size: 13px;
word-break: break-all;
word-wrap: break-word;
/*background-color: #fffaef;*/
border-radius: 4px;
box-shadow: 0px 1px 4px 1px rgba(100, 100, 100, 0.4);
}

+ 1
- 1
docs/docfx.json View File

@@ -50,7 +50,7 @@
"overwrite": "_overwrites/**/**.md",
"globalMetadata": {
"_appTitle": "Discord.Net Documentation",
"_appFooter": "Discord.Net (c) 2015-2018 2.0.0-beta",
"_appFooter": "Discord.Net (c) 2015-2019 2.0.1",
"_enableSearch": true,
"_appLogoPath": "marketing/logo/SVG/Logomark Purple.svg",
"_appFaviconPath": "favicon.ico"


+ 3
- 0
docs/filterConfig.yml View File

@@ -4,4 +4,7 @@ apiRules:
type: Namespace
- exclude:
uidRegex: ^Discord\.Analyzers$
type: Namespace
- exclude:
uidRegex: ^Discord\.API$
type: Namespace

+ 3
- 0
docs/guides/commands/samples/dependency-injection/dependency_map_setup.cs View File

@@ -3,6 +3,9 @@ public class Initialize
private readonly CommandService _commands;
private readonly DiscordSocketClient _client;

// Ask if there are existing CommandService and DiscordSocketClient
// instance. If there are, we retrieve them and add them to the
// DI container; if not, we create our own.
public Initialize(CommandService commands = null, DiscordSocketClient client = null)
{
_commands = commands ?? new CommandService();


+ 2
- 2
docs/guides/commands/samples/dependency-injection/dependency_module.cs View File

@@ -32,6 +32,6 @@ public class DatabaseModule : ModuleBase<SocketCommandContext>
[Command("read")]
public async Task ReadFromDbAsync()
{
await ReplyAsync(_database.GetData());
await ReplyAsync(DbService.GetData());
}
}
}

+ 2
- 11
docs/guides/commands/samples/intro/command_handler.cs View File

@@ -3,6 +3,7 @@ public class CommandHandler
private readonly DiscordSocketClient _client;
private readonly CommandService _commands;

// Retrieve client and CommandService instance via ctor
public CommandHandler(DiscordSocketClient client, CommandService commands)
{
_commands = commands;
@@ -46,19 +47,9 @@ public class CommandHandler

// Execute the command with the command context we just
// created, along with the service provider for precondition checks.
// Keep in mind that result does not indicate a return value
// rather an object stating if the command executed successfully.
var result = await _commands.ExecuteAsync(
await _commands.ExecuteAsync(
context: context,
argPos: argPos,
services: null);

// Optionally, we may inform the user if the command fails
// to be executed; however, this may not always be desired,
// as it may clog up the request queue should a user spam a
// command.
// if (!result.IsSuccess)
// await context.Channel.SendMessageAsync(result.ErrorReason);
}
}

+ 0
- 1
docs/guides/commands/samples/post-execution/command_exception_log.cs View File

@@ -1,6 +1,5 @@
public async Task LogAsync(LogMessage logMessage)
{
// This casting type requries C#7
if (logMessage.Exception is CommandException cmdException)
{
// We can tell the user that something unexpected has happened


+ 3
- 2
docs/guides/commands/samples/typereaders/typereader.cs View File

@@ -1,5 +1,6 @@
// Note: This example is obsolete, a boolean type reader is bundled
// with Discord.Commands
// Please note that the library already supports type reading
// primitive types such as bool. This example is merely used
// to demonstrate how one could write a simple TypeReader.
using Discord;
using Discord.Commands;



+ 0
- 4
docs/guides/concepts/connections.md View File

@@ -35,10 +35,6 @@ sync and has a completed guild cache.

[DiscordSocketClient]: xref:Discord.WebSocket.DiscordSocketClient

### Samples

[!code-csharp[Connection Sample](samples/events.cs)]

## Reconnection

> [!TIP]


+ 1
- 1
docs/guides/concepts/entities.md View File

@@ -7,7 +7,7 @@ title: Entities

> [!NOTE]
> This article is written with the Socket variants of entities in mind,
> not the general interfaces or Rest/Rpc entities.
> not the general interfaces or Rest entities.

Discord.Net provides a versatile entity system for navigating the
Discord API.


+ 0
- 23
docs/guides/concepts/samples/connections.cs View File

@@ -1,23 +0,0 @@
using Discord;
using Discord.WebSocket;

public class Program
{
private DiscordSocketClient _client;
static void Main(string[] args) => new Program().MainAsync().GetAwaiter().GetResult();
public async Task MainAsync()
{
_client = new DiscordSocketClient();

await _client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("DiscordToken"));
await _client.StartAsync();

Console.WriteLine("Press any key to exit...");
Console.ReadKey();

await _client.StopAsync();
// Wait a little for the client to finish disconnecting before allowing the program to return
await Task.Delay(500);
}
}

+ 14
- 19
docs/guides/concepts/samples/logging.cs View File

@@ -1,29 +1,24 @@
using Discord;
using Discord.WebSocket;

public class Program
public class LoggingService
{
private DiscordSocketClient _client;
static void Main(string[] args) => new Program().MainAsync().GetAwaiter().GetResult();
public async Task MainAsync()
public LoggingService(DiscordSocketClient client, CommandService command)
{
_client = new DiscordSocketClient(new DiscordSocketConfig
{
LogLevel = LogSeverity.Info
});

_client.Log += Log;

await _client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("DiscordToken"));
await _client.StartAsync();
await Task.Delay(-1);
client.Log += LogAsync;
command.Log += LogAsync;
}

private Task Log(LogMessage message)
private Task LogAsync(LogMessage message)
{
Console.WriteLine(message.ToString());
if (message.Exception is CommandException cmdException)
{
Console.WriteLine($"[Command/{message.Severity}] {cmdException.Command.Aliases.First()}"
+ $" failed to execute in {cmdException.Context.Channel}.");
Console.WriteLine(cmdException);
}
else
Console.WriteLine($"[General/{message.Severity}] {message}");

return Task.CompletedTask;
}
}

+ 30
- 68
docs/guides/getting_started/first-bot.md View File

@@ -128,12 +128,10 @@ Finally, we can create a new connection to Discord.

Since we are writing a bot, we will be using a [DiscordSocketClient]
along with socket entities. See @Guides.GettingStarted.Terminology
if you are unsure of the differences.

To establish a new connection, we will create an instance of
[DiscordSocketClient] in the new async main. You may pass in an
optional @Discord.WebSocket.DiscordSocketConfig if necessary. For most
users, the default will work fine.
if you are unsure of the differences. To establish a new connection,
we will create an instance of [DiscordSocketClient] in the new async
main. You may pass in an optional @Discord.WebSocket.DiscordSocketConfig
if necessary. For most users, the default will work fine.

Before connecting, we should hook the client's `Log` event to the
log handler that we had just created. Events in Discord.Net work
@@ -142,22 +140,33 @@ similarly to any other events in C#.
Next, you will need to "log in to Discord" with the [LoginAsync]
method with the application's "token."

![Token](images/intro-token.png)

> [!NOTE]
> Pay attention to what you are copying from the developer portal!
> A token is not the same as the application's "client secret."

![Token](images/intro-token.png)

> [!IMPORTANT]
> Your bot's token can be used to gain total access to your bot, so
> **do __NOT__ share this token with anyone else!** It may behoove you
> to store this token in an external source if you plan on distributing
> **do not** share this token with anyone else! You should store this
> token in an external source if you plan on distributing
> the source code for your bot.
>
> In the following example, we retrieve the token from the environment
> variable `DiscordToken`. Please note that this is *not* designed to
> be used in a production environment, as the secrets are stored in
> plain-text.
>
> For information on how to set an environment variable, please see
> instructions below,
>
> * Windows: [How to Create Environment Variables Shortcut in Windows](https://www.tenforums.com/tutorials/121742-create-environment-variables-shortcut-windows.html)
> * Linux: [How To Read and Set Environmental and Shell Variables on a Linux VPS](https://www.digitalocean.com/community/tutorials/how-to-read-and-set-environmental-and-shell-variables-on-a-linux-vps)
> * macOS: [How do I set environment variables on OS X?](https://apple.stackexchange.com/questions/106778/how-do-i-set-environment-variables-on-os-x)

We may now invoke the client's [StartAsync] method, which will
start connection/reconnection logic. It is important to note that
**this method will return as soon as connection logic has been started!**

Any methods that rely on the client's state should go in an event
handler. This means that you should **not** directly be interacting with
the client before it is fully ready.
@@ -173,81 +182,34 @@ The following lines can now be added:
At this point, feel free to start your program and see your bot come
online in Discord.

> [!TIP]
> [!WARNING]
> Getting a warning about `A supplied token was invalid.` and/or
> having trouble logging in? Double-check whether you have put in
> the correct credentials and make sure that it is _not_ a client
> secret, which is different from a token.

> [!TIP]
> [!WARNING]
> Encountering a `PlatformNotSupportedException` when starting your bot?
> This means that you are targeting a platform where .NET's default
> WebSocket client is not supported. Refer to the [installation guide]
> for how to fix this.

> [!NOTE]
> For your reference, you may view the [completed program].

[DiscordSocketClient]: xref:Discord.WebSocket.DiscordSocketClient
[LoginAsync]: xref:Discord.Rest.BaseDiscordClient.LoginAsync*
[StartAsync]: xref:Discord.WebSocket.DiscordSocketClient.StartAsync*
[installation guide]: xref:Guides.GettingStarted.Installation

### Handling a 'ping'

> [!WARNING]
> Please note that this is *not* a proper way to create a command.
> Use the `CommandService` provided by the library instead, as explained
> in the [Command Guide](xref:Guides.Commands.Intro) section.

Now that we have learned to open a connection to Discord, we can
begin handling messages that the users are sending. To start out, our
bot will listen for any message whose content is equal to `!ping` and
will respond back with "Pong!".

Since we want to listen for new messages, the event to hook into
is [MessageReceived].

In your program, add a method that matches the signature of the
`MessageReceived` event - it must be a method (`Func`) that returns
the type `Task` and takes a single parameter, a [SocketMessage]. Also,
since we will be sending data to Discord in this method, we will flag
it as `async`.

In this method, we will add an `if` block to determine if the message
content fits the rules of our scenario - recall that it must be equal
to `!ping`.

Inside the branch of this condition, we will want to send a message,
`Pong!`, back to the channel from which the message comes from. To
find the channel, look for the `Channel` property on the message
parameter.

Next, we will want to send a message to this channel. Since the
channel object is of type [ISocketMessageChannel], we can invoke the
[SendMessageAsync] instance method. For the message content, send back
a string, "Pong!".

You should have now added the following lines,

[!code-csharp[Message](samples/first-bot/message.cs)]

Now that your first bot is complete. You may continue to add on to this
if you desire, but for any bots that will be carrying out multiple
commands, it is strongly recommended to use the command framework as
shown below.

> [!NOTE]
> For your reference, you may view the [completed program].

[MessageReceived]: xref:Discord.WebSocket.BaseSocketClient.MessageReceived
[SocketMessage]: xref:Discord.WebSocket.SocketMessage
[ISocketMessageChannel]: xref:Discord.WebSocket.ISocketMessageChannel
[SendMessageAsync]: xref:Discord.WebSocket.ISocketMessageChannel.SendMessageAsync*
[completed program]: samples/first-bot/complete.cs

# Building a bot with commands

@Guides.Commands.Intro will guide you through how to setup a program
that is ready for [CommandService], a service that is ready for
advanced command usage.
To create commands for your bot, you may choose from a variety of
command processors available. Throughout the guides, we will be using
the one that Discord.Net ships with. @Guides.Commands.Intro will
guide you through how to setup a program that is ready for
[CommandService].

For reference, view an [annotated example] of this structure.



+ 33
- 25
docs/guides/getting_started/installing.md View File

@@ -42,37 +42,45 @@ published to our [MyGet feed]. See

### [Using Visual Studio](#tab/vs-install)

1. Create a new solution for your bot.
1. Create a new solution for your bot
2. In the Solution Explorer, find the "Dependencies" element under your
bot's project.
3. Right click on "Dependencies", and select "Manage NuGet packages."
![Step 3](images/install-vs-deps.png)
4. In the "Browse" tab, search for `Discord.Net`.
5. Install the `Discord.Net` package.
![Step 5](images/install-vs-nuget.png)
bot's project
3. Right click on "Dependencies", and select "Manage NuGet packages"

![Step 3](images/install-vs-deps.png)

4. In the "Browse" tab, search for `Discord.Net`
5. Install the `Discord.Net` package

![Step 5](images/install-vs-nuget.png)

### [Using JetBrains Rider](#tab/rider-install)

1. Create a new solution for your bot.
2. Open the NuGet window (Tools > NuGet > Manage NuGet packages for
Solution).
![Step 2](images/install-rider-nuget-manager.png)
3. In the "Packages" tab, search for `Discord.Net`.
![Step 3](images/install-rider-search.png)
4. Install by adding the package to your project.
![Step 4](images/install-rider-add.png)
1. Create a new solution for your bot
2. Open the NuGet window (Tools > NuGet > Manage NuGet packages for Solution)

![Step 2](images/install-rider-nuget-manager.png)

3. In the "Packages" tab, search for `Discord.Net`

![Step 3](images/install-rider-search.png)

4. Install by adding the package to your project

![Step 4](images/install-rider-add.png)

### [Using Visual Studio Code](#tab/vs-code)

1. Create a new project for your bot.
2. Add `Discord.Net` to your .csproj.
1. Create a new project for your bot
2. Add `Discord.Net` to your `*.csproj`

[!code[Sample .csproj](samples/project.xml)]

### [Using dotnet CLI](#tab/dotnet-cli)

1. Open command-line and navigate to where your .csproj is located.
2. Enter `dotnet add package Discord.Net`.
1. Launch your terminal
2. Navigate to where your `*.csproj` is located
3. Enter `dotnet add package Discord.Net`

***

@@ -115,16 +123,16 @@ by installing one or more custom packages as listed below.

1. Install or compile the following packages:

* `Discord.Net.Providers.WS4Net`
* `Discord.Net.Providers.UDPClient` (Optional)
* This is _only_ required if your bot will be utilizing voice chat.
* `Discord.Net.Providers.WS4Net`
* `Discord.Net.Providers.UDPClient` (Optional)
* This is _only_ required if your bot will be utilizing voice chat.

2. Configure your [DiscordSocketClient] to use these custom providers
over the default ones.

* To do this, set the `WebSocketProvider` and the optional
`UdpSocketProvider` properties on the [DiscordSocketConfig] that you
are passing into your client.
* To do this, set the `WebSocketProvider` and the optional
`UdpSocketProvider` properties on the [DiscordSocketConfig] that you
are passing into your client.

[!code-csharp[Example](samples/netstd11.cs)]



+ 14
- 3
docs/guides/getting_started/nightlies.md View File

@@ -31,22 +31,33 @@ The following is the feed link of Discord.Net,
Depending on which IDE you use, there are many different ways of
adding the feed to your package source.

### [Visual Studio](#tab/vs)
### [Using Visual Studio](#tab/vs)

1. Go to `Tools` > `NuGet Package Manager` > `Package Manager Settings`

![VS](images/nightlies-vs-step1.png)

2. Go to `Package Sources`

![Package Sources](images/nightlies-vs-step2.png)

3. Click on the add icon
4. Fill in the desired name and source as shown below and hit `Update`

![Add Source](images/nightlies-vs-step4.png)

> [!NOTE]
> Remember to tick the `Include prerelease` checkbox to see the
> Remember to tick the `Include pre-release` checkbox to see the
> nightly builds!
> ![Checkbox](images/nightlies-vs-note.png)

### [Local NuGet.Config](#tab/local-nuget-config)
### [Using dotnet CLI](#tab/cli)

1. Launch your terminal
2. Navigate to where your `*.csproj` is located
3. Type `dotnet add package Discord.Net --source https://www.myget.org/F/discord-net/api/v3/index.json`

### [Using Local NuGet.Config](#tab/local-nuget-config)

If you plan on deploying your bot or developing outside of Visual
Studio, you will need to create a local NuGet configuration file for


+ 0
- 10
docs/guides/getting_started/samples/first-bot/complete.cs View File

@@ -9,7 +9,6 @@ public class Program
{
_client = new DiscordSocketClient();
_client.Log += Log;
_client.MessageReceived += MessageReceivedAsync;
await _client.LoginAsync(TokenType.Bot,
Environment.GetEnvironmentVariable("DiscordToken"));
await _client.StartAsync();
@@ -17,15 +16,6 @@ public class Program
// Block this task until the program is closed.
await Task.Delay(-1);
}

private async Task MessageReceivedAsync(SocketMessage message)
{
if (message.Content == "!ping")
{
await message.Channel.SendMessageAsync("Pong!");
}
}

private Task Log(LogMessage msg)
{
Console.WriteLine(msg.ToString());


+ 3
- 6
docs/guides/introduction/intro.md View File

@@ -16,17 +16,14 @@ understand these topics to some extent before proceeding. With all
that being said, feel free to visit us on Discord at the link below
if you have any questions!

Here are some examples:

1. [Official samples]
2. [Official template]
An official collection of samples can be found
in [our GitHub repository].

> [!NOTE]
> Please note that you should *not* try to blindly copy paste
> the code. The examples are meant to be a template or a guide.

[Official template]: https://github.com/foxbot/DiscordBotBase/tree/csharp/src/DiscordBot
[Official samples]: https://github.com/RogueException/Discord.Net/tree/dev/samples
[our GitHub repository]: https://github.com/RogueException/Discord.Net/tree/dev/samples
[Task-based Asynchronous Pattern]: https://docs.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/task-based-asynchronous-pattern-tap
[polymorphism]: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/polymorphism
[interface]: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/interfaces/


+ 1
- 0
samples/02_commands_framework/Modules/PublicModule.cs View File

@@ -60,6 +60,7 @@ namespace _02_commands_framework.Modules
public Task ListAsync(params string[] objects)
=> ReplyAsync("You listed: " + string.Join("; ", objects));

// Setting a custom ErrorMessage property will help clarify the precondition error
[Command("guild_only")]
[RequireContext(ContextType.Guild, ErrorMessage = "Sorry, this command must be ran from within a server, not a DM!")]
public Task GuildOnlyCommand()


+ 3
- 1
samples/02_commands_framework/Program.cs View File

@@ -37,10 +37,12 @@ namespace _02_commands_framework
client.Log += LogAsync;
services.GetRequiredService<CommandService>().Log += LogAsync;

// Tokens should be considered secret data, and never hard-coded.
// Tokens should be considered secret data and never hard-coded.
// We can read from the environment variable to avoid hardcoding.
await client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token"));
await client.StartAsync();

// Here we initialize the logic required to register our commands.
await services.GetRequiredService<CommandHandlingService>().InitializeAsync();

await Task.Delay(-1);


+ 15
- 3
samples/02_commands_framework/Services/CommandHandlingService.cs View File

@@ -20,12 +20,16 @@ namespace _02_commands_framework.Services
_discord = services.GetRequiredService<DiscordSocketClient>();
_services = services;

// Hook CommandExecuted to handle post-command-execution logic.
_commands.CommandExecuted += CommandExecutedAsync;
// Hook MessageReceived so we can process each message to see
// if it qualifies as a command.
_discord.MessageReceived += MessageReceivedAsync;
}

public async Task InitializeAsync()
{
// Register modules that are public and inherit ModuleBase<T>.
await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services);
}

@@ -37,10 +41,18 @@ namespace _02_commands_framework.Services

// This value holds the offset where the prefix ends
var argPos = 0;
// Perform prefix check. You may want to replace this with
// (!message.HasCharPrefix('!', ref argPos))
// for a more traditional command format like !help.
if (!message.HasMentionPrefix(_discord.CurrentUser, ref argPos)) return;

var context = new SocketCommandContext(_discord, message);
await _commands.ExecuteAsync(context, argPos, _services); // we will handle the result in CommandExecutedAsync
// Perform the execution of the command. In this method,
// the command service will perform precondition and parsing check
// then execute the command if one is matched.
await _commands.ExecuteAsync(context, argPos, _services);
// Note that normally a result will be returned by this format, but here
// we will handle the result in CommandExecutedAsync,
}

public async Task CommandExecutedAsync(Optional<CommandInfo> command, ICommandContext context, IResult result)
@@ -49,12 +61,12 @@ namespace _02_commands_framework.Services
if (!command.IsSpecified)
return;

// the command was succesful, we don't care about this result, unless we want to log that a command succeeded.
// the command was successful, we don't care about this result, unless we want to log that a command succeeded.
if (result.IsSuccess)
return;

// the command failed, let's notify the user that something happened.
await context.Channel.SendMessageAsync($"error: {result.ToString()}");
await context.Channel.SendMessageAsync($"error: {result}");
}
}
}

+ 5
- 0
src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs View File

@@ -17,7 +17,12 @@ namespace Discord.Commands
/// Gets the specified <see cref="Discord.ChannelPermission" /> of the precondition.
/// </summary>
public ChannelPermission? ChannelPermission { get; }
/// <inheritdoc />
public override string ErrorMessage { get; set; }
/// <summary>
/// Gets or sets the error message if the precondition
/// fails due to being run outside of a Guild channel.
/// </summary>
public string NotAGuildErrorMessage { get; set; }

/// <summary>


+ 1
- 0
src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs View File

@@ -33,6 +33,7 @@ namespace Discord.Commands
/// Gets the context required to execute the command.
/// </summary>
public ContextType Contexts { get; }
/// <inheritdoc />
public override string ErrorMessage { get; set; }

/// <summary> Requires the command to be invoked in the specified context. </summary>


+ 1
- 0
src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs View File

@@ -30,6 +30,7 @@ namespace Discord.Commands
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class RequireNsfwAttribute : PreconditionAttribute
{
/// <inheritdoc />
public override string ErrorMessage { get; set; }

/// <inheritdoc />


+ 1
- 0
src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs View File

@@ -34,6 +34,7 @@ namespace Discord.Commands
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class RequireOwnerAttribute : PreconditionAttribute
{
/// <inheritdoc />
public override string ErrorMessage { get; set; }

/// <inheritdoc />


+ 5
- 0
src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs View File

@@ -17,7 +17,12 @@ namespace Discord.Commands
/// Gets the specified <see cref="Discord.ChannelPermission" /> of the precondition.
/// </summary>
public ChannelPermission? ChannelPermission { get; }
/// <inheritdoc />
public override string ErrorMessage { get; set; }
/// <summary>
/// Gets or sets the error message if the precondition
/// fails due to being run outside of a Guild channel.
/// </summary>
public string NotAGuildErrorMessage { get; set; }

/// <summary>


+ 2
- 9
src/Discord.Net.Commands/CommandService.cs View File

@@ -39,15 +39,8 @@ namespace Discord.Commands
/// Occurs when a command is successfully executed without any error.
/// </summary>
/// <remarks>
/// <para>
/// This event is fired when a command has been successfully executed without any of the following errors:
/// </para>
/// <para>* Parsing error</para>
/// <para>* Precondition error</para>
/// <para>* Runtime exception</para>
/// <para>
/// Should the command encounter any of the aforementioned error, this event will not be raised.
/// </para>
/// This event is fired when a command has been executed, successfully or not. When a command fails to
/// execute during parsing or precondition stage, the CommandInfo may not be returned.
/// </remarks>
public event Func<Optional<CommandInfo>, ICommandContext, IResult, Task> CommandExecuted { add { _commandExecutedEvent.Add(value); } remove { _commandExecutedEvent.Remove(value); } }
internal readonly AsyncEvent<Func<Optional<CommandInfo>, ICommandContext, IResult, Task>> _commandExecutedEvent = new AsyncEvent<Func<Optional<CommandInfo>, ICommandContext, IResult, Task>>();


+ 3
- 3
src/Discord.Net.Core/Discord.Net.Core.csproj View File

@@ -11,8 +11,8 @@
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="System.Collections.Immutable" Version="1.3.1" />
<PackageReference Include="System.Interactive.Async" Version="3.1.1" />
</ItemGroup>
<ItemGroup Condition=" '$(Configuration)' != 'Release' ">
<PackageReference Include="IDisposableAnalyzers" Version="2.0.3.3" />
<PackageReference Include="IDisposableAnalyzers" Version="2.1.2">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>

+ 3
- 1
src/Discord.Net.Core/Entities/Channels/ChannelType.cs View File

@@ -12,6 +12,8 @@ namespace Discord
/// <summary> The channel is a group channel. </summary>
Group = 3,
/// <summary> The channel is a category channel. </summary>
Category = 4
Category = 4,
/// <summary> The channel is a news channel. </summary>
News = 5
}
}

+ 8
- 36
src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs View File

@@ -97,26 +97,12 @@ namespace Discord
/// Adds or updates the permission overwrite for the given role.
/// </summary>
/// <example>
/// The following example fetches a role via <see cref="IGuild.GetRole"/> and a channel via
/// <para>The following example fetches a role via <see cref="IGuild.GetRole"/> and a channel via
/// <see cref="IGuild.GetChannelAsync"/>. Next, it checks if an overwrite had already been set via
/// <see cref="GetPermissionOverwrite(Discord.IRole)"/>; if not, it denies the role from sending any
/// messages to the channel.
/// <code lang="cs">
/// // Fetches the role and channels
/// var role = guild.GetRole(339805618376540160);
/// var channel = await guild.GetChannelAsync(233937283911385098);
///
/// // If either the of the object does not exist, bail
/// if (role == null || channel == null) return;
///
/// // Fetches the previous overwrite and bail if one is found
/// var previousOverwrite = channel.GetPermissionOverwrite(role);
/// if (previousOverwrite.HasValue) return;
///
/// // Creates a new OverwritePermissions with send message set to deny and pass it into the method
/// await channel.AddPermissionOverwriteAsync(role,
/// new OverwritePermissions(sendMessage: PermValue.Deny));
/// </code>
/// messages to the channel.</para>
/// <code language="cs" region="AddPermissionOverwriteAsyncRole"
/// source="..\..\..\Discord.Net.Examples\Core\Entities\Channels\IGuildChannel.Examples.cs"/>
/// </example>
/// <param name="role">The role to add the overwrite to.</param>
/// <param name="permissions">The overwrite to add to the role.</param>
@@ -130,26 +116,12 @@ namespace Discord
/// Adds or updates the permission overwrite for the given user.
/// </summary>
/// <example>
/// The following example fetches a user via <see cref="IGuild.GetUserAsync"/> and a channel via
/// <para>The following example fetches a user via <see cref="IGuild.GetUserAsync"/> and a channel via
/// <see cref="IGuild.GetChannelAsync"/>. Next, it checks if an overwrite had already been set via
/// <see cref="GetPermissionOverwrite(Discord.IUser)"/>; if not, it denies the user from sending any
/// messages to the channel.
/// <code lang="cs">
/// // Fetches the role and channels
/// var user = await guild.GetUserAsync(168693960628371456);
/// var channel = await guild.GetChannelAsync(233937283911385098);
///
/// // If either the of the object does not exist, bail
/// if (user == null || channel == null) return;
///
/// // Fetches the previous overwrite and bail if one is found
/// var previousOverwrite = channel.GetPermissionOverwrite(user);
/// if (previousOverwrite.HasValue) return;
///
/// // Creates a new OverwritePermissions with send message set to deny and pass it into the method
/// await channel.AddPermissionOverwriteAsync(role,
/// new OverwritePermissions(sendMessage: PermValue.Deny));
/// </code>
/// messages to the channel.</para>
/// <code language="cs" region="AddPermissionOverwriteAsyncUser"
/// source="..\..\..\Discord.Net.Examples\Core\Entities\Channels\IGuildChannel.Examples.cs"/>
/// </example>
/// <param name="user">The user to add the overwrite to.</param>
/// <param name="permissions">The overwrite to add to the user.</param>


+ 37
- 48
src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs View File

@@ -14,13 +14,10 @@ namespace Discord
/// Sends a message to this message channel.
/// </summary>
/// <example>
/// The following example sends a message with the current system time in RFC 1123 format to the channel and
/// deletes itself after 5 seconds.
/// <code language="cs">
/// var message = await channel.SendMessageAsync(DateTimeOffset.UtcNow.ToString("R"));
/// await Task.Delay(TimeSpan.FromSeconds(5))
/// .ContinueWith(x => message.DeleteAsync());
/// </code>
/// <para>The following example sends a message with the current system time in RFC 1123 format to the channel and
/// deletes itself after 5 seconds.</para>
/// <code language="cs" region="SendMessageAsync"
/// source="..\..\..\Discord.Net.Examples\Core\Entities\Channels\IMessageChannel.Examples.cs" />
/// </example>
/// <param name="text">The message to be sent.</param>
/// <param name="isTTS">Determines whether the message should be read aloud by Discord or not.</param>
@@ -35,18 +32,14 @@ namespace Discord
/// Sends a file to this message channel with an optional caption.
/// </summary>
/// <example>
/// The following example uploads a local file called <c>wumpus.txt</c> along with the text
/// <c>good discord boi</c> to the channel.
/// <code language="cs">
/// await channel.SendFileAsync("wumpus.txt", "good discord boi");
/// </code>
///
/// The following example uploads a local image called <c>b1nzy.jpg</c> embedded inside a rich embed to the
/// channel.
/// <code language="cs">
/// await channel.SendFileAsync("b1nzy.jpg",
/// embed: new EmbedBuilder {ImageUrl = "attachment://b1nzy.jpg"}.Build());
/// </code>
/// <para>The following example uploads a local file called <c>wumpus.txt</c> along with the text
/// <c>good discord boi</c> to the channel.</para>
/// <code language="cs" region="SendFileAsync.FilePath"
/// source="..\..\..\Discord.Net.Examples\Core\Entities\Channels\IMessageChannel.Examples.cs" />
/// <para>The following example uploads a local image called <c>b1nzy.jpg</c> embedded inside a rich embed to the
/// channel.</para>
/// <code language="cs" region="SendFileAsync.FilePath.EmbeddedImage"
/// source="..\..\..\Discord.Net.Examples\Core\Entities\Channels\IMessageChannel.Examples.cs" />
/// </example>
/// <remarks>
/// This method sends a file as if you are uploading an attachment directly from your Discord client.
@@ -61,21 +54,20 @@ namespace Discord
/// <param name="isTTS">Whether the message should be read aloud by Discord or not.</param>
/// <param name="embed">The <see cref="Discord.EmbedType.Rich" /> <see cref="Embed" /> to be sent.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <param name="isSpoiler">Whether the message attachment should be hidden as a spoiler.</param>
/// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message.
/// </returns>
Task<IUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null);
Task<IUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false);
/// <summary>
/// Sends a file to this message channel with an optional caption.
/// </summary>
/// <example>
/// The following example uploads a streamed image that will be called <c>b1nzy.jpg</c> embedded inside a
/// rich embed to the channel.
/// <code language="cs">
/// await channel.SendFileAsync(b1nzyStream, "b1nzy.jpg",
/// embed: new EmbedBuilder {ImageUrl = "attachment://b1nzy.jpg"}.Build());
/// </code>
/// <para>The following example uploads a streamed image that will be called <c>b1nzy.jpg</c> embedded inside a
/// rich embed to the channel.</para>
/// <code language="cs" region="SendFileAsync.FileStream.EmbeddedImage"
/// source="..\..\..\Discord.Net.Examples\Core\Entities\Channels\IMessageChannel.Examples.cs" />
/// </example>
/// <remarks>
/// This method sends a file as if you are uploading an attachment directly from your Discord client.
@@ -91,11 +83,12 @@ namespace Discord
/// <param name="isTTS">Whether the message should be read aloud by Discord or not.</param>
/// <param name="embed">The <see cref="Discord.EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <param name="isSpoiler">Whether the message attachment should be hidden as a spoiler.</param>
/// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message.
/// </returns>
Task<IUserMessage> SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null);
Task<IUserMessage> SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false);

/// <summary>
/// Gets a message from this message channel.
@@ -130,12 +123,10 @@ namespace Discord
/// of flattening.
/// </remarks>
/// <example>
/// The following example downloads 300 messages and gets messages that belong to the user
/// <c>53905483156684800</c>.
/// <code lang="cs">
/// var messages = await messageChannel.GetMessagesAsync(300).FlattenAsync();
/// var userMessages = messages.Where(x =&gt; x.Author.Id == 53905483156684800);
/// </code>
/// <para>The following example downloads 300 messages and gets messages that belong to the user
/// <c>53905483156684800</c>.</para>
/// <code language="cs" region="GetMessagesAsync.FromLimit.Standard"
/// source="..\..\..\Discord.Net.Examples\Core\Entities\Channels\IMessageChannel.Examples.cs" />
/// </example>
/// <param name="limit">The numbers of message to be gotten from.</param>
/// <param name="mode">The <see cref="CacheMode" /> that determines whether the object should be fetched from
@@ -168,10 +159,13 @@ namespace Discord
/// of flattening.
/// </remarks>
/// <example>
/// The following example gets 5 message prior to the message identifier <c>442012544660537354</c>.
/// <code lang="cs">
/// var messages = await channel.GetMessagesAsync(442012544660537354, Direction.Before, 5).FlattenAsync();
/// </code>
/// <para>The following example gets 5 message prior to the message identifier <c>442012544660537354</c>.</para>
/// <code language="cs" region="GetMessagesAsync.FromId.FromMessage"
/// source="..\..\..\Discord.Net.Examples\Core\Entities\Channels\IMessageChannel.Examples.cs" />
/// <para>The following example attempts to retrieve <c>messageCount</c> number of messages from the
/// beginning of the channel and prints them to the console.</para>
/// <code language="cs" region="GetMessagesAsync.FromId.BeginningMessages"
/// source="..\..\..\Discord.Net.Examples\Core\Entities\Channels\IMessageChannel.Examples.cs" />
/// </example>
/// <param name="fromMessageId">The ID of the starting message to get the messages from.</param>
/// <param name="dir">The direction of the messages to be gotten from.</param>
@@ -206,10 +200,9 @@ namespace Discord
/// of flattening.
/// </remarks>
/// <example>
/// The following example gets 5 message prior to a specific message, <c>oldMessage</c>.
/// <code lang="cs">
/// var messages = await channel.GetMessagesAsync(oldMessage, Direction.Before, 5).FlattenAsync();
/// </code>
/// <para>The following example gets 5 message prior to a specific message, <c>oldMessage</c>.</para>
/// <code language="cs" region="GetMessagesAsync.FromMessage"
/// source="..\..\..\Discord.Net.Examples\Core\Entities\Channels\IMessageChannel.Examples.cs" />
/// </example>
/// <param name="fromMessage">The starting message to get the messages from.</param>
/// <param name="dir">The direction of the messages to be gotten from.</param>
@@ -262,13 +255,9 @@ namespace Discord
/// object is disposed.
/// </summary>
/// <example>
/// The following example keeps the client in the typing state until <c>LongRunningAsync</c> has finished.
/// <code lang="cs">
/// using (messageChannel.EnterTypingState())
/// {
/// await LongRunningAsync();
/// }
/// </code>
/// <para>The following example keeps the client in the typing state until <c>LongRunningAsync</c> has finished.</para>
/// <code language="cs" region="EnterTypingState"
/// source="..\..\..\Discord.Net.Examples\Core\Entities\Channels\IMessageChannel.Examples.cs" />
/// </example>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>


+ 1
- 1
src/Discord.Net.Core/Entities/Channels/ITextChannel.cs View File

@@ -40,7 +40,7 @@ namespace Discord
/// </summary>
/// <example>
/// The following example gets 250 messages from the channel and deletes them.
/// <code lang="cs">
/// <code language="cs">
/// var messages = await textChannel.GetMessagesAsync(250).FlattenAsync();
/// await textChannel.DeleteMessagesAsync(messages);
/// </code>


+ 2
- 10
src/Discord.Net.Core/Entities/Guilds/IGuild.cs View File

@@ -440,16 +440,8 @@ namespace Discord
/// </summary>
/// <example>
/// The following example creates a new text channel under an existing category named <c>Wumpus</c> with a set topic.
/// <code lang="cs">
/// var categories = await guild.GetCategoriesAsync();
/// var targetCategory = categories.FirstOrDefault(x => x.Name == "wumpus");
/// if (targetCategory == null) return;
/// await Context.Guild.CreateTextChannelAsync(name, x =>
/// {
/// x.CategoryId = targetCategory.Id;
/// x.Topic = $"This channel was created at {DateTimeOffset.UtcNow} by {user}.";
/// });
/// </code>
/// <code language="cs" region="CreateTextChannelAsync"
/// source="..\..\..\Discord.Net.Examples\Core\Entities\Guilds\IGuild.Examples.cs"/>
/// </example>
/// <param name="name">The new name for the text channel.</param>
/// <param name="func">The delegate containing the properties to be applied to the channel upon its creation.</param>


+ 5
- 1
src/Discord.Net.Core/Entities/Messages/MessageType.cs View File

@@ -32,6 +32,10 @@ namespace Discord
/// <summary>
/// The message when another message is pinned.
/// </summary>
ChannelPinnedMessage = 6
ChannelPinnedMessage = 6,
/// <summary>
/// The message when a new member joined.
/// </summary>
GuildMemberJoin = 7
}
}

+ 9
- 20
src/Discord.Net.Core/Entities/Users/IUser.cs View File

@@ -23,10 +23,8 @@ namespace Discord
/// <example>
/// The following example attempts to retrieve the user's current avatar and send it to a channel; if one is
/// not set, a default avatar for this user will be returned instead.
/// <code language="cs">
/// var userAvatarUrl = user.GetAvatarUrl() ?? user.GetDefaultAvatarUrl();
/// await textChannel.SendMessageAsync(userAvatarUrl);
/// </code>
/// <code language="cs" region="GetAvatarUrl"
/// source="..\..\..\Discord.Net.Examples\Core\Entities\Users\IUser.Examples.cs"/>
/// </example>
/// <param name="format">The format to return.</param>
/// <param name="size">The size of the image to return in. This can be any power of two between 16 and 2048.
@@ -84,27 +82,18 @@ namespace Discord
/// <remarks>
/// This method is used to obtain or create a channel used to send a direct message.
/// <note type="warning">
/// In event that the current user cannot send a message to the target user, a channel can and will still be
/// created by Discord. However, attempting to send a message will yield a
/// <see cref="Discord.Net.HttpException"/> with a 403 as its
/// <see cref="Discord.Net.HttpException.HttpCode"/>. There are currently no official workarounds by
/// Discord.
/// In event that the current user cannot send a message to the target user, a channel can and will
/// still be created by Discord. However, attempting to send a message will yield a
/// <see cref="Discord.Net.HttpException"/> with a 403 as its
/// <see cref="Discord.Net.HttpException.HttpCode"/>. There are currently no official workarounds by
/// Discord.
/// </note>
/// </remarks>
/// <example>
/// The following example attempts to send a direct message to the target user and logs the incident should
/// it fail.
/// <code language="cs">
/// var channel = await user.GetOrCreateDMChannelAsync();
/// try
/// {
/// await channel.SendMessageAsync("Awesome stuff!");
/// }
/// catch (Discord.Net.HttpException ex) when (ex.HttpCode == 403)
/// {
/// Console.WriteLine($"Boo, I cannot message {user}");
/// }
/// </code>
/// <code region="GetOrCreateDMChannelAsync" language="cs"
/// source="../../../Discord.Net.Examples/Core/Entities/Users/IUser.Examples.cs"/>
/// </example>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>


+ 15
- 0
src/Discord.Net.Core/Extensions/AttachmentExtensions.cs View File

@@ -0,0 +1,15 @@
namespace Discord
{
public static class AttachmentExtensions
{
/// <summary>
/// The prefix applied to files to indicate that it is a spoiler.
/// </summary>
public const string SpoilerPrefix = "SPOILER_";
/// <summary>
/// Gets whether the message's attachments are spoilers or not.
/// </summary>
public static bool IsSpoiler(this IAttachment attachment)
=> attachment.Filename.StartsWith(SpoilerPrefix);
}
}

+ 0
- 4
src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs View File

@@ -29,10 +29,6 @@ namespace Discord
public static EmbedBuilder WithAuthor(this EmbedBuilder builder, IUser user) =>
builder.WithAuthor($"{user.Username}#{user.Discriminator}", user.GetAvatarUrl());

/// <summary> Fills the embed author field with the provided user's nickname and avatar URL; username is used if nickname is not set. </summary>
public static EmbedBuilder WithAuthor(this EmbedBuilder builder, IGuildUser user) =>
builder.WithAuthor($"{user.Nickname ?? user.Username}#{user.Discriminator}", user.GetAvatarUrl());

/// <summary> Converts a <see cref="EmbedType.Rich"/> <see cref="IEmbed"/> object to a <see cref="EmbedBuilder"/>. </summary>
/// <exception cref="InvalidOperationException">The embed type is not <see cref="EmbedType.Rich"/>.</exception>
public static EmbedBuilder ToEmbedBuilder(this IEmbed embed)


+ 3
- 1
src/Discord.Net.Core/Format.cs View File

@@ -4,7 +4,7 @@ namespace Discord
public static class Format
{
// Characters which need escaping
private static readonly string[] SensitiveCharacters = { "\\", "*", "_", "~", "`" };
private static readonly string[] SensitiveCharacters = { "\\", "*", "_", "~", "`", "|" };

/// <summary> Returns a markdown-formatted string with bold formatting. </summary>
public static string Bold(string text) => $"**{text}**";
@@ -14,6 +14,8 @@ namespace Discord
public static string Underline(string text) => $"__{text}__";
/// <summary> Returns a markdown-formatted string with strikethrough formatting. </summary>
public static string Strikethrough(string text) => $"~~{text}~~";
/// <summary> Returns a string with spoiler formatting. </summary>
public static string Spoiler(string text) => $"||{text}||";
/// <summary> Returns a markdown-formatted URL. Only works in <see cref="EmbedBuilder"/> descriptions and fields. </summary>
public static string Url(string text, string url) => $"[{text}]({url})";
/// <summary> Escapes a URL so that a preview is not generated. </summary>


+ 4
- 4
src/Discord.Net.Core/IDiscordClient.cs View File

@@ -63,7 +63,7 @@ namespace Discord
/// Gets a generic channel.
/// </summary>
/// <example>
/// <code lang="cs" title="Example method">
/// <code language="cs" title="Example method">
/// var channel = await _client.GetChannelAsync(381889909113225237);
/// if (channel != null &amp;&amp; channel is IMessageChannel msgChannel)
/// {
@@ -194,7 +194,7 @@ namespace Discord
/// Gets a user.
/// </summary>
/// <example>
/// <code lang="cs" title="Example method">
/// <code language="cs" title="Example method">
/// var user = await _client.GetUserAsync(168693960628371456);
/// if (user != null)
/// Console.WriteLine($"{user} is created at {user.CreatedAt}.";
@@ -212,7 +212,7 @@ namespace Discord
/// Gets a user.
/// </summary>
/// <example>
/// <code lang="cs" title="Example method">
/// <code language="cs" title="Example method">
/// var user = await _client.GetUserAsync("Still", "2876");
/// if (user != null)
/// Console.WriteLine($"{user} is created at {user.CreatedAt}.";
@@ -232,7 +232,7 @@ namespace Discord
/// </summary>
/// <example>
/// The following example gets the most optimal voice region from the collection.
/// <code lang="cs">
/// <code language="cs">
/// var regions = await client.GetVoiceRegionsAsync();
/// var optimalRegion = regions.FirstOrDefault(x =&gt; x.IsOptimal);
/// </code>


+ 66
- 1
src/Discord.Net.Core/Utils/TokenUtils.cs View File

@@ -17,6 +17,47 @@ namespace Discord
/// </remarks>
internal const int MinBotTokenLength = 58;

internal const char Base64Padding = '=';

/// <summary>
/// Pads a base64-encoded string with 0, 1, or 2 '=' characters,
/// if the string is not a valid multiple of 4.
/// Does not ensure that the provided string contains only valid base64 characters.
/// Strings that already contain padding will not have any more padding applied.
/// </summary>
/// <remarks>
/// A string that would require 3 padding characters is considered to be already corrupt.
/// Some older bot tokens may require padding, as the format provided by Discord
/// does not include this padding in the token.
/// </remarks>
/// <param name="encodedBase64">The base64 encoded string to pad with characters.</param>
/// <returns>A string containing the base64 padding.</returns>
/// <exception cref="FormatException">
/// Thrown if <paramref name="encodedBase64"/> would require an invalid number of padding characters.
/// </exception>
/// <exception cref="ArgumentNullException">
/// Thrown if <paramref name="encodedBase64"/> is null, empty, or whitespace.
/// </exception>
internal static string PadBase64String(string encodedBase64)
{
if (string.IsNullOrWhiteSpace(encodedBase64))
throw new ArgumentNullException(paramName: encodedBase64,
message: "The supplied base64-encoded string was null or whitespace.");

// do not pad if already contains padding characters
if (encodedBase64.IndexOf(Base64Padding) != -1)
return encodedBase64;

// based from https://stackoverflow.com/a/1228744
var padding = (4 - (encodedBase64.Length % 4)) % 4;
if (padding == 3)
// can never have 3 characters of padding
throw new FormatException("The provided base64 string is corrupt, as it requires an invalid amount of padding.");
else if (padding == 0)
return encodedBase64;
return encodedBase64.PadRight(encodedBase64.Length + padding, Base64Padding);
}

/// <summary>
/// Decodes a base 64 encoded string into a ulong value.
/// </summary>
@@ -29,6 +70,8 @@ namespace Discord

try
{
// re-add base64 padding if missing
encoded = PadBase64String(encoded);
// decode the base64 string
var bytes = Convert.FromBase64String(encoded);
var idStr = Encoding.UTF8.GetString(bytes);
@@ -46,7 +89,7 @@ namespace Discord
}
catch (ArgumentException)
{
// ignore exception, can be thrown by BitConverter
// ignore exception, can be thrown by BitConverter, or by PadBase64String
}
return null;
}
@@ -76,6 +119,25 @@ namespace Discord
return DecodeBase64UserId(segments[0]).HasValue;
}

/// <summary>
/// The set of all characters that are not allowed inside of a token.
/// </summary>
internal static char[] IllegalTokenCharacters = new char[]
{
' ', '\t', '\r', '\n'
};

/// <summary>
/// Checks if the given token contains a whitespace or newline character
/// that would fail to log in.
/// </summary>
/// <param name="token"> The token to validate. </param>
/// <returns>
/// True if the token contains a whitespace or newline character.
/// </returns>
internal static bool CheckContainsIllegalCharacters(string token)
=> token.IndexOfAny(IllegalTokenCharacters) != -1;

/// <summary>
/// Checks the validity of the supplied token of a specific type.
/// </summary>
@@ -88,6 +150,9 @@ namespace Discord
// A Null or WhiteSpace token of any type is invalid.
if (string.IsNullOrWhiteSpace(token))
throw new ArgumentNullException(paramName: nameof(token), message: "A token cannot be null, empty, or contain only whitespace.");
// ensure that there are no whitespace or newline characters
if (CheckContainsIllegalCharacters(token))
throw new ArgumentException(message: "The token contains a whitespace or newline character. Ensure that the token has been properly trimmed.", paramName: nameof(token));

switch (tokenType)
{


+ 46
- 0
src/Discord.Net.Examples/Core/Entities/Channels/IGuildChannel.Examples.cs View File

@@ -0,0 +1,46 @@
using System;
using System.Threading.Tasks;
using JetBrains.Annotations;

namespace Discord.Net.Examples.Core.Entities.Channels
{
[PublicAPI]
internal class GuildChannelExamples
{
#region AddPermissionOverwriteAsyncRole

public async Task MuteRoleAsync(IRole role, IGuildChannel channel)
{
if (role == null) throw new ArgumentNullException(nameof(role));
if (channel == null) throw new ArgumentNullException(nameof(channel));

// Fetches the previous overwrite and bail if one is found
var previousOverwrite = channel.GetPermissionOverwrite(role);
if (previousOverwrite.HasValue && previousOverwrite.Value.SendMessages == PermValue.Deny)
throw new InvalidOperationException($"Role {role.Name} had already been muted in this channel.");

// Creates a new OverwritePermissions with send message set to deny and pass it into the method
await channel.AddPermissionOverwriteAsync(role, new OverwritePermissions(sendMessages: PermValue.Deny));
}

#endregion

#region AddPermissionOverwriteAsyncUser

public async Task MuteUserAsync(IGuildUser user, IGuildChannel channel)
{
if (user == null) throw new ArgumentNullException(nameof(user));
if (channel == null) throw new ArgumentNullException(nameof(channel));

// Fetches the previous overwrite and bail if one is found
var previousOverwrite = channel.GetPermissionOverwrite(user);
if (previousOverwrite.HasValue && previousOverwrite.Value.SendMessages == PermValue.Deny)
throw new InvalidOperationException($"User {user.Username} had already been muted in this channel.");

// Creates a new OverwritePermissions with send message set to deny and pass it into the method
await channel.AddPermissionOverwriteAsync(user, new OverwritePermissions(sendMessages: PermValue.Deny));
}

#endregion
}
}

+ 114
- 0
src/Discord.Net.Examples/Core/Entities/Channels/IMessageChannel.Examples.cs View File

@@ -0,0 +1,114 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using JetBrains.Annotations;

namespace Discord.Net.Examples.Core.Entities.Channels
{
[PublicAPI]
internal class MessageChannelExamples
{
#region GetMessagesAsync.FromId.BeginningMessages

public async Task PrintFirstMessages(IMessageChannel channel, int messageCount)
{
// Although the library does attempt to divide the messageCount by 100
// to comply to Discord's maximum message limit per request, sending
// too many could still cause the queue to clog up.
// The purpose of this exception is to discourage users from sending
// too many requests at once.
if (messageCount > 1000)
throw new InvalidOperationException("Too many messages requested.");

// Setting fromMessageId to 0 will make Discord
// default to the first message in channel.
var messages = await channel.GetMessagesAsync(
0, Direction.After, messageCount)
.FlattenAsync();

// Print message content
foreach (var message in messages)
Console.WriteLine($"{message.Author} posted '{message.Content}' at {message.CreatedAt}.");
}

#endregion

public async Task GetMessagesExampleBody(IMessageChannel channel)
{
#pragma warning disable IDISP001
#pragma warning disable IDISP014
// We're just declaring this for the sample below.
// Ideally, you want to get or create your HttpClient
// from IHttpClientFactory.
// You get a bonus for reading the example source though!
var httpClient = new HttpClient();
#pragma warning restore IDISP014
#pragma warning restore IDISP001

// Another dummy method
Task LongRunningAsync()
{
return Task.Delay(0);
}

#region GetMessagesAsync.FromLimit.Standard

var messages = await channel.GetMessagesAsync(300).FlattenAsync();
var userMessages = messages.Where(x => x.Author.Id == 53905483156684800);

#endregion

#region GetMessagesAsync.FromMessage

var oldMessage = await channel.SendMessageAsync("boi");
var messagesFromMsg = await channel.GetMessagesAsync(oldMessage, Direction.Before, 5).FlattenAsync();

#endregion


#region GetMessagesAsync.FromId.FromMessage

await channel.GetMessagesAsync(442012544660537354, Direction.Before, 5).FlattenAsync();

#endregion

#region SendMessageAsync

var message = await channel.SendMessageAsync(DateTimeOffset.UtcNow.ToString("R"));
await Task.Delay(TimeSpan.FromSeconds(5))
.ContinueWith(x => message.DeleteAsync());

#endregion

#region SendFileAsync.FilePath

await channel.SendFileAsync("wumpus.txt", "good discord boi");

#endregion

#region SendFileAsync.FilePath.EmbeddedImage

await channel.SendFileAsync("b1nzy.jpg",
embed: new EmbedBuilder {ImageUrl = "attachment://b1nzy.jpg"}.Build());

#endregion


#region SendFileAsync.FileStream.EmbeddedImage

using (var b1nzyStream = await httpClient.GetStreamAsync("https://example.com/b1nzy"))
await channel.SendFileAsync(b1nzyStream, "b1nzy.jpg",
embed: new EmbedBuilder {ImageUrl = "attachment://b1nzy.jpg"}.Build());

#endregion

#region EnterTypingState

using (channel.EnterTypingState()) await LongRunningAsync();

#endregion

}
}
}

+ 25
- 0
src/Discord.Net.Examples/Core/Entities/Guilds/IGuild.Examples.cs View File

@@ -0,0 +1,25 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using JetBrains.Annotations;

namespace Discord.Net.Examples.Core.Entities.Guilds
{
[PublicAPI]
internal class GuildExamples
{
#region CreateTextChannelAsync
public async Task CreateTextChannelUnderWumpus(IGuild guild, string name)
{
var categories = await guild.GetCategoriesAsync();
var targetCategory = categories.FirstOrDefault(x => x.Name == "wumpus");
if (targetCategory == null) return;
await guild.CreateTextChannelAsync(name, x =>
{
x.CategoryId = targetCategory.Id;
x.Topic = $"This channel was created at {DateTimeOffset.UtcNow}.";
});
}
#endregion
}
}

+ 38
- 0
src/Discord.Net.Examples/Core/Entities/Users/IUser.Examples.cs View File

@@ -0,0 +1,38 @@
using System;
using System.Net;
using System.Threading.Tasks;
using JetBrains.Annotations;

namespace Discord.Net.Examples.Core.Entities.Users
{
[PublicAPI]
internal class UserExamples
{
#region GetAvatarUrl

public async Task GetAvatarAsync(IUser user, ITextChannel textChannel)
{
var userAvatarUrl = user.GetAvatarUrl() ?? user.GetDefaultAvatarUrl();
await textChannel.SendMessageAsync(userAvatarUrl);
}

#endregion

#region GetOrCreateDMChannelAsync

public async Task MessageUserAsync(IUser user)
{
var channel = await user.GetOrCreateDMChannelAsync();
try
{
await channel.SendMessageAsync("Awesome stuff!");
}
catch (Discord.Net.HttpException ex) when (ex.HttpCode == HttpStatusCode.Forbidden)
{
Console.WriteLine($"Boo, I cannot message {user}.");
}
}

#endregion
}
}

+ 21
- 0
src/Discord.Net.Examples/Discord.Net.Examples.csproj View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<Compile Remove="Core\Entities\Guilds\IGuild.Examples.cs" />
</ItemGroup>

<ItemGroup>
<None Include="Core\Entities\Guilds\IGuild.Examples.cs" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" />
<ProjectReference Include="..\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" />
<PackageReference Include="JetBrains.Annotations" Version="2018.3.0" />
</ItemGroup>

</Project>

+ 117
- 0
src/Discord.Net.Examples/WebSocket/BaseSocketClient.Events.Examples.cs View File

@@ -0,0 +1,117 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Discord.WebSocket;
using JetBrains.Annotations;

namespace Discord.Net.Examples.WebSocket
{
[PublicAPI]
internal class BaseSocketClientExamples
{
#region ReactionAdded

public void HookReactionAdded(BaseSocketClient client)
=> client.ReactionAdded += HandleReactionAddedAsync;

public async Task HandleReactionAddedAsync(Cacheable<IUserMessage, ulong> cachedMessage,
ISocketMessageChannel originChannel, SocketReaction reaction)
{
var message = await cachedMessage.GetOrDownloadAsync();
if (message != null && reaction.User.IsSpecified)
Console.WriteLine($"{reaction.User.Value} just added a reaction '{reaction.Emote}' " +
$"to {message.Author}'s message ({message.Id}).");
}

#endregion

#region ChannelCreated

public void HookChannelCreated(BaseSocketClient client)
=> client.ChannelCreated += HandleChannelCreated;

public Task HandleChannelCreated(SocketChannel channel)
{
if (channel is SocketGuildChannel guildChannel)
Console.WriteLine($"A new channel '{guildChannel.Name}'({guildChannel.Id}, {guildChannel.GetType()})"
+ $"has been created at {guildChannel.CreatedAt}.");
return Task.CompletedTask;
}

#endregion

#region ChannelDestroyed

public void HookChannelDestroyed(BaseSocketClient client)
=> client.ChannelDestroyed += HandleChannelDestroyed;

public Task HandleChannelDestroyed(SocketChannel channel)
{
if (channel is SocketGuildChannel guildChannel)
Console.WriteLine(
$"A new channel '{guildChannel.Name}'({guildChannel.Id}, {guildChannel.GetType()}) has been deleted.");
return Task.CompletedTask;
}

#endregion

#region ChannelUpdated

public void HookChannelUpdated(BaseSocketClient client)
=> client.ChannelUpdated += HandleChannelRename;

public Task HandleChannelRename(SocketChannel beforeChannel, SocketChannel afterChannel)
{
if (beforeChannel is SocketGuildChannel beforeGuildChannel &&
afterChannel is SocketGuildChannel afterGuildChannel)
if (beforeGuildChannel.Name != afterGuildChannel.Name)
Console.WriteLine(
$"A channel ({beforeChannel.Id}) is renamed from {beforeGuildChannel.Name} to {afterGuildChannel.Name}.");
return Task.CompletedTask;
}

#endregion

#region MessageReceived

private readonly ulong[] _targetUserIds = {168693960628371456, 53905483156684800};

public void HookMessageReceived(BaseSocketClient client)
=> client.MessageReceived += HandleMessageReceived;

public Task HandleMessageReceived(SocketMessage message)
{
// check if the message is a user message as opposed to a system message (e.g. Clyde, pins, etc.)
if (!(message is SocketUserMessage userMessage)) return Task.CompletedTask;
// check if the message origin is a guild message channel
if (!(userMessage.Channel is SocketTextChannel textChannel)) return Task.CompletedTask;
// check if the target user was mentioned
var targetUsers = userMessage.MentionedUsers.Where(x => _targetUserIds.Contains(x.Id));
foreach (var targetUser in targetUsers)
Console.WriteLine(
$"{targetUser} was mentioned in the message '{message.Content}' by {message.Author} in {textChannel.Name}.");
return Task.CompletedTask;
}

#endregion

#region MessageDeleted

public void HookMessageDeleted(BaseSocketClient client)
=> client.MessageDeleted += HandleMessageDelete;

public Task HandleMessageDelete(Cacheable<IMessage, ulong> cachedMessage, ISocketMessageChannel channel)
{
// check if the message exists in cache; if not, we cannot report what was removed
if (!cachedMessage.HasValue) return Task.CompletedTask;
var message = cachedMessage.Value;
Console.WriteLine(
$"A message ({message.Id}) from {message.Author} was removed from the channel {channel.Name} ({channel.Id}):"
+ Environment.NewLine
+ message.Content);
return Task.CompletedTask;
}

#endregion
}
}

+ 7
- 1
src/Discord.Net.Rest/API/Rest/UploadFileParams.cs View File

@@ -19,6 +19,7 @@ namespace Discord.API.Rest
public Optional<string> Nonce { get; set; }
public Optional<bool> IsTTS { get; set; }
public Optional<Embed> Embed { get; set; }
public bool IsSpoiler { get; set; } = false;

public UploadFileParams(Stream file)
{
@@ -28,7 +29,10 @@ namespace Discord.API.Rest
public IReadOnlyDictionary<string, object> ToDictionary()
{
var d = new Dictionary<string, object>();
d["file"] = new MultipartFile(File, Filename.GetValueOrDefault("unknown.dat"));
var filename = Filename.GetValueOrDefault("unknown.dat");
if (IsSpoiler && !filename.StartsWith(AttachmentExtensions.SpoilerPrefix))
filename = filename.Insert(0, AttachmentExtensions.SpoilerPrefix);
d["file"] = new MultipartFile(File, filename);

var payload = new Dictionary<string, object>();
if (Content.IsSpecified)
@@ -39,6 +43,8 @@ namespace Discord.API.Rest
payload["nonce"] = Nonce.Value;
if (Embed.IsSpecified)
payload["embed"] = Embed.Value;
if (IsSpoiler)
payload["hasSpoiler"] = IsSpoiler.ToString();

var json = new StringBuilder();
using (var text = new StringWriter(json))


+ 7
- 1
src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs View File

@@ -22,6 +22,8 @@ namespace Discord.API.Rest
public Optional<string> AvatarUrl { get; set; }
public Optional<Embed[]> Embeds { get; set; }

public bool IsSpoiler { get; set; } = false;

public UploadWebhookFileParams(Stream file)
{
File = file;
@@ -30,7 +32,11 @@ namespace Discord.API.Rest
public IReadOnlyDictionary<string, object> ToDictionary()
{
var d = new Dictionary<string, object>();
d["file"] = new MultipartFile(File, Filename.GetValueOrDefault("unknown.dat"));
var filename = Filename.GetValueOrDefault("unknown.dat");
if (IsSpoiler && !filename.StartsWith(AttachmentExtensions.SpoilerPrefix))
filename = filename.Insert(0, AttachmentExtensions.SpoilerPrefix);

d["file"] = new MultipartFile(File, filename);

var payload = new Dictionary<string, object>();
if (Content.IsSpecified)


+ 11
- 3
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs View File

@@ -1,4 +1,4 @@
using Model = Discord.API.AuditLog;
using Model = Discord.API.AuditLog;
using EntryModel = Discord.API.AuditLogEntry;

namespace Discord.Rest
@@ -8,15 +8,16 @@ namespace Discord.Rest
/// </summary>
public class MessageDeleteAuditLogData : IAuditLogData
{
private MessageDeleteAuditLogData(ulong channelId, int count)
private MessageDeleteAuditLogData(ulong channelId, int count, ulong authorId)
{
ChannelId = channelId;
MessageCount = count;
AuthorId = authorId;
}

internal static MessageDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
{
return new MessageDeleteAuditLogData(entry.Options.MessageDeleteChannelId.Value, entry.Options.MessageDeleteCount.Value);
return new MessageDeleteAuditLogData(entry.Options.MessageDeleteChannelId.Value, entry.Options.MessageDeleteCount.Value, entry.TargetId.Value);
}

/// <summary>
@@ -34,5 +35,12 @@ namespace Discord.Rest
/// deleted from.
/// </returns>
public ulong ChannelId { get; }
/// <summary>
/// Gets the author of the messages that were deleted.
/// </summary>
/// <returns>
/// A <see cref="ulong"/> representing the snowflake identifier for the user that created the deleted messages.
/// </returns>
public ulong AuthorId { get; }
}
}

+ 14
- 5
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookCreateAuditLogData.cs View File

@@ -10,9 +10,10 @@ namespace Discord.Rest
/// </summary>
public class WebhookCreateAuditLogData : IAuditLogData
{
private WebhookCreateAuditLogData(IWebhook webhook, WebhookType type, string name, ulong channelId)
private WebhookCreateAuditLogData(IWebhook webhook, ulong webhookId, WebhookType type, string name, ulong channelId)
{
Webhook = webhook;
WebhookId = webhookId;
Name = name;
Type = type;
ChannelId = channelId;
@@ -31,23 +32,31 @@ namespace Discord.Rest
var name = nameModel.NewValue.ToObject<string>(discord.ApiClient.Serializer);

var webhookInfo = log.Webhooks?.FirstOrDefault(x => x.Id == entry.TargetId);
var webhook = RestWebhook.Create(discord, (IGuild)null, webhookInfo);
var webhook = webhookInfo == null ? null : RestWebhook.Create(discord, (IGuild)null, webhookInfo);

return new WebhookCreateAuditLogData(webhook, type, name, channelId);
return new WebhookCreateAuditLogData(webhook, entry.TargetId.Value, type, name, channelId);
}

// Doc Note: Corresponds to the *current* data

/// <summary>
/// Gets the webhook that was created.
/// Gets the webhook that was created if it still exists.
/// </summary>
/// <returns>
/// A webhook object representing the webhook that was created.
/// A webhook object representing the webhook that was created if it still exists, otherwise returns <c>null</c>.
/// </returns>
public IWebhook Webhook { get; }

// Doc Note: Corresponds to the *audit log* data

/// <summary>
/// Gets the webhook id.
/// </summary>
/// <returns>
/// The webhook identifier.
/// </returns>
public ulong WebhookId { get; }

/// <summary>
/// Gets the type of webhook that was created.
/// </summary>


+ 4
- 4
src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs View File

@@ -199,18 +199,18 @@ namespace Discord.Rest
/// <exception cref="IOException">An I/O error occurred while opening the file.</exception>
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public static async Task<RestUserMessage> SendFileAsync(IMessageChannel channel, BaseDiscordClient client,
string filePath, string text, bool isTTS, Embed embed, RequestOptions options)
string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler)
{
string filename = Path.GetFileName(filePath);
using (var file = File.OpenRead(filePath))
return await SendFileAsync(channel, client, file, filename, text, isTTS, embed, options).ConfigureAwait(false);
return await SendFileAsync(channel, client, file, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false);
}

/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public static async Task<RestUserMessage> SendFileAsync(IMessageChannel channel, BaseDiscordClient client,
Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options)
Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler)
{
var args = new UploadFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS, Embed = embed != null ? embed.ToModel() : Optional<API.Embed>.Unspecified };
var args = new UploadFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS, Embed = embed != null ? embed.ToModel() : Optional<API.Embed>.Unspecified, IsSpoiler = isSpoiler };
var model = await client.ApiClient.UploadFileAsync(channel.Id, args, options).ConfigureAwait(false);
return RestUserMessage.Create(client, channel, client.CurrentUser, model);
}


+ 3
- 3
src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs View File

@@ -42,12 +42,12 @@ namespace Discord.Rest
/// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message.
/// </returns>
new Task<RestUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null);
new Task<RestUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false);
/// <summary>
/// Sends a file to this message channel with an optional caption.
/// </summary>
/// <remarks>
/// This method follows the same behavior as described in <see cref="IMessageChannel.SendFileAsync(Stream, string, string, bool, Embed, RequestOptions)"/>.
/// This method follows the same behavior as described in <see cref="IMessageChannel.SendFileAsync(Stream, string, string, bool, Embed, RequestOptions, bool)"/>.
/// Please visit its documentation for more details on this method.
/// </remarks>
/// <param name="stream">The <see cref="Stream" /> of the file to be sent.</param>
@@ -60,7 +60,7 @@ namespace Discord.Rest
/// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message.
/// </returns>
new Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null);
new Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false);

/// <summary>
/// Gets a message from this message channel.


+ 1
- 0
src/Discord.Net.Rest/Entities/Channels/RestChannel.cs View File

@@ -23,6 +23,7 @@ namespace Discord.Rest
{
switch (model.Type)
{
case ChannelType.News:
case ChannelType.Text:
case ChannelType.Voice:
return RestGuildChannel.Create(discord, new RestGuild(discord, model.GuildId.Value), model);


+ 8
- 8
src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs View File

@@ -121,12 +121,12 @@ namespace Discord.Rest
/// <exception cref="NotSupportedException"><paramref name="filePath" /> is in an invalid format.</exception>
/// <exception cref="IOException">An I/O error occurred while opening the file.</exception>
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null)
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options);
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false)
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options, isSpoiler);
/// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null)
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options);
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false)
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options, isSpoiler);

/// <inheritdoc />
public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null)
@@ -200,11 +200,11 @@ namespace Discord.Rest
async Task<IReadOnlyCollection<IMessage>> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options)
=> await GetPinnedMessagesAsync(options).ConfigureAwait(false);
/// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options)
=> await SendFileAsync(filePath, text, isTTS, embed, options).ConfigureAwait(false);
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler)
=> await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false);
/// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options)
=> await SendFileAsync(stream, filename, text, isTTS, embed, options).ConfigureAwait(false);
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler)
=> await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false);
/// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options)
=> await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false);


+ 8
- 8
src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs View File

@@ -123,12 +123,12 @@ namespace Discord.Rest
/// <exception cref="NotSupportedException"><paramref name="filePath" /> is in an invalid format.</exception>
/// <exception cref="IOException">An I/O error occurred while opening the file.</exception>
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null)
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options);
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false)
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options, isSpoiler);
/// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null)
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options);
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false)
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options, isSpoiler);

/// <inheritdoc />
public Task TriggerTypingAsync(RequestOptions options = null)
@@ -178,11 +178,11 @@ namespace Discord.Rest
async Task<IReadOnlyCollection<IMessage>> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options)
=> await GetPinnedMessagesAsync(options).ConfigureAwait(false);

async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options)
=> await SendFileAsync(filePath, text, isTTS, embed, options).ConfigureAwait(false);
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler)
=> await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false);

async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options)
=> await SendFileAsync(stream, filename, text, isTTS, embed, options).ConfigureAwait(false);
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler)
=> await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false);
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options)
=> await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false);



+ 9
- 7
src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs View File

@@ -15,7 +15,7 @@ namespace Discord.Rest
private ImmutableArray<Overwrite> _overwrites;

/// <inheritdoc />
public IReadOnlyCollection<Overwrite> PermissionOverwrites => _overwrites;
public virtual IReadOnlyCollection<Overwrite> PermissionOverwrites => _overwrites;

internal IGuild Guild { get; }
/// <inheritdoc />
@@ -34,6 +34,8 @@ namespace Discord.Rest
{
switch (model.Type)
{
case ChannelType.News:
return RestNewsChannel.Create(discord, guild, model);
case ChannelType.Text:
return RestTextChannel.Create(discord, guild, model);
case ChannelType.Voice:
@@ -79,7 +81,7 @@ namespace Discord.Rest
/// <returns>
/// An overwrite object for the targeted user; <c>null</c> if none is set.
/// </returns>
public OverwritePermissions? GetPermissionOverwrite(IUser user)
public virtual OverwritePermissions? GetPermissionOverwrite(IUser user)
{
for (int i = 0; i < _overwrites.Length; i++)
{
@@ -96,7 +98,7 @@ namespace Discord.Rest
/// <returns>
/// An overwrite object for the targeted role; <c>null</c> if none is set.
/// </returns>
public OverwritePermissions? GetPermissionOverwrite(IRole role)
public virtual OverwritePermissions? GetPermissionOverwrite(IRole role)
{
for (int i = 0; i < _overwrites.Length; i++)
{
@@ -115,7 +117,7 @@ namespace Discord.Rest
/// <returns>
/// A task representing the asynchronous permission operation for adding the specified permissions to the channel.
/// </returns>
public async Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null)
public virtual async Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null)
{
await ChannelHelper.AddPermissionOverwriteAsync(this, Discord, user, permissions, options).ConfigureAwait(false);
_overwrites = _overwrites.Add(new Overwrite(user.Id, PermissionTarget.User, new OverwritePermissions(permissions.AllowValue, permissions.DenyValue)));
@@ -129,7 +131,7 @@ namespace Discord.Rest
/// <returns>
/// A task representing the asynchronous permission operation for adding the specified permissions to the channel.
/// </returns>
public async Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null)
public virtual async Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null)
{
await ChannelHelper.AddPermissionOverwriteAsync(this, Discord, role, permissions, options).ConfigureAwait(false);
_overwrites = _overwrites.Add(new Overwrite(role.Id, PermissionTarget.Role, new OverwritePermissions(permissions.AllowValue, permissions.DenyValue)));
@@ -143,7 +145,7 @@ namespace Discord.Rest
/// <returns>
/// A task representing the asynchronous operation for removing the specified permissions from the channel.
/// </returns>
public async Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null)
public virtual async Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null)
{
await ChannelHelper.RemovePermissionOverwriteAsync(this, Discord, user, options).ConfigureAwait(false);

@@ -164,7 +166,7 @@ namespace Discord.Rest
/// <returns>
/// A task representing the asynchronous operation for removing the specified permissions from the channel.
/// </returns>
public async Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null)
public virtual async Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null)
{
await ChannelHelper.RemovePermissionOverwriteAsync(this, Discord, role, options).ConfigureAwait(false);



+ 53
- 0
src/Discord.Net.Rest/Entities/Channels/RestNewsChannel.cs View File

@@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Model = Discord.API.Channel;

namespace Discord.Rest
{
/// <summary>
/// Represents a REST-based news channel in a guild that has the same properties as a <see cref="RestTextChannel"/>.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class RestNewsChannel : RestTextChannel
{
internal RestNewsChannel(BaseDiscordClient discord, IGuild guild, ulong id)
:base(discord, guild, id)
{
}
internal new static RestNewsChannel Create(BaseDiscordClient discord, IGuild guild, Model model)
{
var entity = new RestNewsChannel(discord, guild, model.Id);
entity.Update(model);
return entity;
}
public override int SlowModeInterval => throw new NotSupportedException("News channels do not support Slow Mode.");
public override Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null)
{
throw new NotSupportedException("News channels do not support Overwrite Permissions.");
}
public override Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null)
{
throw new NotSupportedException("News channels do not support Overwrite Permissions.");
}
public override OverwritePermissions? GetPermissionOverwrite(IRole role)
{
throw new NotSupportedException("News channels do not support Overwrite Permissions.");
}
public override OverwritePermissions? GetPermissionOverwrite(IUser user)
{
throw new NotSupportedException("News channels do not support Overwrite Permissions.");
}
public override Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null)
{
throw new NotSupportedException("News channels do not support Overwrite Permissions.");
}
public override Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null)
{
throw new NotSupportedException("News channels do not support Overwrite Permissions.");
}
}
}

+ 9
- 9
src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs View File

@@ -17,7 +17,7 @@ namespace Discord.Rest
/// <inheritdoc />
public string Topic { get; private set; }
/// <inheritdoc />
public int SlowModeInterval { get; private set; }
public virtual int SlowModeInterval { get; private set; }
/// <inheritdoc />
public ulong? CategoryId { get; private set; }

@@ -129,13 +129,13 @@ namespace Discord.Rest
/// <exception cref="NotSupportedException"><paramref name="filePath" /> is in an invalid format.</exception>
/// <exception cref="IOException">An I/O error occurred while opening the file.</exception>
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null)
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options);
public Task<RestUserMessage> SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false)
=> ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options, isSpoiler);

/// <inheritdoc />
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null)
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options);
public Task<RestUserMessage> SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false)
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options, isSpoiler);

/// <inheritdoc />
public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null)
@@ -266,12 +266,12 @@ namespace Discord.Rest
=> await GetPinnedMessagesAsync(options).ConfigureAwait(false);

/// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options)
=> await SendFileAsync(filePath, text, isTTS, embed, options).ConfigureAwait(false);
async Task<IUserMessage> IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler)
=> await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false);

/// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options)
=> await SendFileAsync(stream, filename, text, isTTS, embed, options).ConfigureAwait(false);
async Task<IUserMessage> IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler)
=> await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false);
/// <inheritdoc />
async Task<IUserMessage> IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options)
=> await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false);


+ 1
- 1
src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs View File

@@ -405,7 +405,7 @@ namespace Discord.Rest
/// </summary>
/// <example>
/// The following example creates a new text channel under an existing category named <c>Wumpus</c> with a set topic.
/// <code lang="cs">
/// <code language="cs">
/// var categories = await guild.GetCategoriesAsync();
/// var targetCategory = categories.FirstOrDefault(x => x.Name == "wumpus");
/// if (targetCategory == null) return;


+ 4
- 4
src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs View File

@@ -16,10 +16,10 @@ namespace Discord.Rest
{
private bool _isMentioningEveryone, _isTTS, _isPinned;
private long? _editedTimestampTicks;
private ImmutableArray<Attachment> _attachments;
private ImmutableArray<Embed> _embeds;
private ImmutableArray<ITag> _tags;
private ImmutableArray<RestReaction> _reactions;
private ImmutableArray<Attachment> _attachments = ImmutableArray.Create<Attachment>();
private ImmutableArray<Embed> _embeds = ImmutableArray.Create<Embed>();
private ImmutableArray<ITag> _tags = ImmutableArray.Create<ITag>();
private ImmutableArray<RestReaction> _reactions = ImmutableArray.Create<RestReaction>();

/// <inheritdoc />
public override bool IsTTS => _isTTS;


+ 2
- 2
src/Discord.Net.WebSocket/API/Gateway/MessageDeleteBulkEvent.cs View File

@@ -1,4 +1,4 @@
#pragma warning disable CS1591
#pragma warning disable CS1591
using Newtonsoft.Json;
using System.Collections.Generic;

@@ -9,6 +9,6 @@ namespace Discord.API.Gateway
[JsonProperty("channel_id")]
public ulong ChannelId { get; set; }
[JsonProperty("ids")]
public IEnumerable<ulong> Ids { get; set; }
public ulong[] Ids { get; set; }
}
}

+ 83
- 0
src/Discord.Net.WebSocket/BaseSocketClient.Events.cs View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Discord.WebSocket
@@ -18,6 +19,10 @@ namespace Discord.WebSocket
/// see the derived classes of <see cref="SocketChannel"/> for more details.
/// </para>
/// </remarks>
/// <example>
/// <code language="cs" region="ChannelCreated"
/// source="..\Discord.Net.Examples\WebSocket\BaseSocketClient.Events.Examples.cs"/>
/// </example>
public event Func<SocketChannel, Task> ChannelCreated
{
add { _channelCreatedEvent.Add(value); }
@@ -36,6 +41,10 @@ namespace Discord.WebSocket
/// see the derived classes of <see cref="SocketChannel"/> for more details.
/// </para>
/// </remarks>
/// <example>
/// <code language="cs" region="ChannelDestroyed"
/// source="..\Discord.Net.Examples\WebSocket\BaseSocketClient.Events.Examples.cs"/>
/// </example>
public event Func<SocketChannel, Task> ChannelDestroyed {
add { _channelDestroyedEvent.Add(value); }
remove { _channelDestroyedEvent.Remove(value); }
@@ -54,6 +63,10 @@ namespace Discord.WebSocket
/// <see cref="SocketChannel"/> for more details.
/// </para>
/// </remarks>
/// <example>
/// <code language="cs" region="ChannelUpdated"
/// source="..\Discord.Net.Examples\WebSocket\BaseSocketClient.Events.Examples.cs"/>
/// </example>
public event Func<SocketChannel, SocketChannel, Task> ChannelUpdated {
add { _channelUpdatedEvent.Add(value); }
remove { _channelUpdatedEvent.Remove(value); }
@@ -74,6 +87,11 @@ namespace Discord.WebSocket
/// derived classes of <see cref="SocketMessage"/> for more details.
/// </para>
/// </remarks>
/// <example>
/// <para>The example below checks if the newly received message contains the target user.</para>
/// <code language="cs" region="MessageReceived"
/// source="..\Discord.Net.Examples\WebSocket\BaseSocketClient.Events.Examples.cs"/>
/// </example>
public event Func<SocketMessage, Task> MessageReceived {
add { _messageReceivedEvent.Add(value); }
remove { _messageReceivedEvent.Remove(value); }
@@ -102,11 +120,47 @@ namespace Discord.WebSocket
/// <see cref="ISocketMessageChannel"/> parameter.
/// </para>
/// </remarks>
/// <example>
/// <code language="cs" region="MessageDeleted"
/// source="..\Discord.Net.Examples\WebSocket\BaseSocketClient.Events.Examples.cs" />
/// </example>
public event Func<Cacheable<IMessage, ulong>, ISocketMessageChannel, Task> MessageDeleted {
add { _messageDeletedEvent.Add(value); }
remove { _messageDeletedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<Cacheable<IMessage, ulong>, ISocketMessageChannel, Task>> _messageDeletedEvent = new AsyncEvent<Func<Cacheable<IMessage, ulong>, ISocketMessageChannel, Task>>();
/// <summary> Fired when multiple messages are bulk deleted. </summary>
/// <remarks>
/// <note>
/// The <see cref="MessageDeleted"/> event will not be fired for individual messages contained in this event.
/// </note>
/// <para>
/// This event is fired when multiple messages are bulk deleted. The event handler must return a
/// <see cref="Task"/> and accept an <see cref="IReadOnlyCollection{Cacheable{TEntity,TId}}"/> and
/// <see cref="ISocketMessageChannel"/> as its parameters.
/// </para>
/// <para>
/// <note type="important">
/// It is not possible to retrieve the message via
/// <see cref="Cacheable{TEntity,TId}.DownloadAsync"/>; the message cannot be retrieved by Discord
/// after the message has been deleted.
/// </note>
/// If caching is enabled via <see cref="DiscordSocketConfig"/>, the
/// <see cref="Cacheable{TEntity,TId}"/> entity will contain the deleted message; otherwise, in event
/// that the message cannot be retrieved, the snowflake ID of the message is preserved in the
/// <see cref="ulong"/>.
/// </para>
/// <para>
/// The source channel of the removed message will be passed into the
/// <see cref="ISocketMessageChannel"/> parameter.
/// </para>
/// </remarks>
public event Func<IReadOnlyCollection<Cacheable<IMessage, ulong>>, ISocketMessageChannel, Task> MessagesBulkDeleted
{
add { _messagesBulkDeletedEvent.Add(value); }
remove { _messagesBulkDeletedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<IReadOnlyCollection<Cacheable<IMessage, ulong>>, ISocketMessageChannel, Task>> _messagesBulkDeletedEvent = new AsyncEvent<Func<IReadOnlyCollection<Cacheable<IMessage, ulong>>, ISocketMessageChannel, Task>>();
/// <summary> Fired when a message is updated. </summary>
/// <remarks>
/// <para>
@@ -134,6 +188,35 @@ namespace Discord.WebSocket
}
internal readonly AsyncEvent<Func<Cacheable<IMessage, ulong>, SocketMessage, ISocketMessageChannel, Task>> _messageUpdatedEvent = new AsyncEvent<Func<Cacheable<IMessage, ulong>, SocketMessage, ISocketMessageChannel, Task>>();
/// <summary> Fired when a reaction is added to a message. </summary>
/// <remarks>
/// <para>
/// This event is fired when a reaction is added to a user message. The event handler must return a
/// <see cref="Task"/> and accept a <see cref="Cacheable{TEntity,TId}"/>, an
/// <see cref="ISocketMessageChannel"/>, and a <see cref="SocketReaction"/> as its parameter.
/// </para>
/// <para>
/// If caching is enabled via <see cref="DiscordSocketConfig"/>, the
/// <see cref="Cacheable{TEntity,TId}"/> entity will contain the original message; otherwise, in event
/// that the message cannot be retrieved, the snowflake ID of the message is preserved in the
/// <see cref="ulong"/>.
/// </para>
/// <para>
/// The source channel of the reaction addition will be passed into the
/// <see cref="ISocketMessageChannel"/> parameter.
/// </para>
/// <para>
/// The reaction that was added will be passed into the <see cref="SocketReaction"/> parameter.
/// </para>
/// <note>
/// When fetching the reaction from this event, a user may not be provided under
/// <see cref="SocketReaction.User"/>. Please see the documentation of the property for more
/// information.
/// </note>
/// </remarks>
/// <example>
/// <code language="cs" region="ReactionAdded"
/// source="..\Discord.Net.Examples\WebSocket\BaseSocketClient.Events.Examples.cs"/>
/// </example>
public event Func<Cacheable<IUserMessage, ulong>, ISocketMessageChannel, SocketReaction, Task> ReactionAdded {
add { _reactionAddedEvent.Add(value); }
remove { _reactionAddedEvent.Remove(value); }


+ 14
- 10
src/Discord.Net.WebSocket/BaseSocketClient.cs View File

@@ -97,13 +97,15 @@ namespace Discord.WebSocket
/// <remarks>
/// This method gets the user present in the WebSocket cache with the given condition.
/// <note type="warning">
/// Sometimes a user may return <c>null</c> due to Discord not sending offline users in large
/// guilds (i.e. guild with 100+ members) actively. To download users on startup, consider enabling
/// <see cref="DiscordSocketConfig.AlwaysDownloadUsers"/>.
/// Sometimes a user may return <c>null</c> due to Discord not sending offline users in large guilds
/// (i.e. guild with 100+ members) actively. To download users on startup and to see more information
/// about this subject, see <see cref="Discord.WebSocket.DiscordSocketConfig.AlwaysDownloadUsers" />.
/// </note>
/// <note>
/// This method does not attempt to fetch users that the logged-in user does not have access to (i.e.
/// users who don't share mutual guild(s) with the current user).
/// users who don't share mutual guild(s) with the current user). If you wish to get a user that you do
/// not have access to, consider using the REST implementation of
/// <see cref="DiscordRestClient.GetUserAsync(System.UInt64,Discord.RequestOptions)" />.
/// </note>
/// </remarks>
/// <returns>
@@ -114,20 +116,22 @@ namespace Discord.WebSocket
/// <summary>
/// Gets a user.
/// </summary>
/// <param name="username">The name of the user.</param>
/// <param name="discriminator">The discriminator value of the user.</param>
/// <remarks>
/// This method gets the user present in the WebSocket cache with the given condition.
/// <note type="warning">
/// Sometimes a user may return <c>null</c> due to Discord not sending offline users in large
/// guilds (i.e. guild with 100+ members) actively. To download users on startup, consider enabling
/// <see cref="DiscordSocketConfig.AlwaysDownloadUsers"/>.
/// Sometimes a user may return <c>null</c> due to Discord not sending offline users in large guilds
/// (i.e. guild with 100+ members) actively. To download users on startup and to see more information
/// about this subject, see <see cref="Discord.WebSocket.DiscordSocketConfig.AlwaysDownloadUsers" />.
/// </note>
/// <note>
/// This method does not attempt to fetch users that the logged-in user does not have access to (i.e.
/// users who don't share mutual guild(s) with the current user).
/// users who don't share mutual guild(s) with the current user). If you wish to get a user that you do
/// not have access to, consider using the REST implementation of
/// <see cref="DiscordRestClient.GetUserAsync(System.UInt64,Discord.RequestOptions)" />.
/// </note>
/// </remarks>
/// <param name="username">The name of the user.</param>
/// <param name="discriminator">The discriminator value of the user.</param>
/// <returns>
/// A generic WebSocket-based user; <c>null</c> when the user cannot be found.
/// </returns>


Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save