Started learning crystal and made an OOP example for a freind
This commit is contained in:
25
C#/this dot/this dot.sln
Normal file
25
C#/this dot/this dot.sln
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio 15
|
||||||
|
VisualStudioVersion = 15.0.27428.2043
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "this dot", "this dot\this dot.csproj", "{2119289F-E327-4138-82E7-414D9FB8DD9E}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{2119289F-E327-4138-82E7-414D9FB8DD9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{2119289F-E327-4138-82E7-414D9FB8DD9E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{2119289F-E327-4138-82E7-414D9FB8DD9E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{2119289F-E327-4138-82E7-414D9FB8DD9E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {70988D6A-4E61-4366-8D8E-7440E079C2AB}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
6
C#/this dot/this dot/App.config
Normal file
6
C#/this dot/this dot/App.config
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<configuration>
|
||||||
|
<startup>
|
||||||
|
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
|
||||||
|
</startup>
|
||||||
|
</configuration>
|
||||||
43
C#/this dot/this dot/Program.cs
Normal file
43
C#/this dot/this dot/Program.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace this_dot {
|
||||||
|
class Program {
|
||||||
|
static void Main() {
|
||||||
|
Person one = new Person("Dave", 16);
|
||||||
|
Person two = new Person("John", 21, one);
|
||||||
|
Person three = new Person("Chris", 12, two);
|
||||||
|
Person four = new Person("Vero", 23, three);
|
||||||
|
|
||||||
|
Person[] ppl = { one, two, three, four };
|
||||||
|
|
||||||
|
foreach (Person person in ppl) {
|
||||||
|
this.getOlder();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Person {
|
||||||
|
public string Name { get; set; }
|
||||||
|
public int Age { get; set; }
|
||||||
|
public Person Parent { get; set; }
|
||||||
|
|
||||||
|
public Person(string _name, int _age, Person _parent) {
|
||||||
|
Name = _name;
|
||||||
|
Age = _age;
|
||||||
|
Parent = _parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Person(string _name, int _age) {
|
||||||
|
Name = _name;
|
||||||
|
Age = _age;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void getOlder() {
|
||||||
|
Age++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
C#/this dot/this dot/Properties/AssemblyInfo.cs
Normal file
36
C#/this dot/this dot/Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
// General Information about an assembly is controlled through the following
|
||||||
|
// set of attributes. Change these attribute values to modify the information
|
||||||
|
// associated with an assembly.
|
||||||
|
[assembly: AssemblyTitle("this dot")]
|
||||||
|
[assembly: AssemblyDescription("")]
|
||||||
|
[assembly: AssemblyConfiguration("")]
|
||||||
|
[assembly: AssemblyCompany("")]
|
||||||
|
[assembly: AssemblyProduct("this dot")]
|
||||||
|
[assembly: AssemblyCopyright("Copyright © 2018")]
|
||||||
|
[assembly: AssemblyTrademark("")]
|
||||||
|
[assembly: AssemblyCulture("")]
|
||||||
|
|
||||||
|
// Setting ComVisible to false makes the types in this assembly not visible
|
||||||
|
// to COM components. If you need to access a type in this assembly from
|
||||||
|
// COM, set the ComVisible attribute to true on that type.
|
||||||
|
[assembly: ComVisible(false)]
|
||||||
|
|
||||||
|
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||||
|
[assembly: Guid("2119289f-e327-4138-82e7-414d9fb8dd9e")]
|
||||||
|
|
||||||
|
// Version information for an assembly consists of the following four values:
|
||||||
|
//
|
||||||
|
// Major Version
|
||||||
|
// Minor Version
|
||||||
|
// Build Number
|
||||||
|
// Revision
|
||||||
|
//
|
||||||
|
// You can specify all the values or you can default the Build and Revision Numbers
|
||||||
|
// by using the '*' as shown below:
|
||||||
|
// [assembly: AssemblyVersion("1.0.*")]
|
||||||
|
[assembly: AssemblyVersion("1.0.0.0")]
|
||||||
|
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||||
52
C#/this dot/this dot/this dot.csproj
Normal file
52
C#/this dot/this dot/this dot.csproj
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||||
|
<PropertyGroup>
|
||||||
|
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||||
|
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||||
|
<ProjectGuid>{2119289F-E327-4138-82E7-414D9FB8DD9E}</ProjectGuid>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<RootNamespace>this_dot</RootNamespace>
|
||||||
|
<AssemblyName>this dot</AssemblyName>
|
||||||
|
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
|
||||||
|
<FileAlignment>512</FileAlignment>
|
||||||
|
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||||
|
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||||
|
<DebugSymbols>true</DebugSymbols>
|
||||||
|
<DebugType>full</DebugType>
|
||||||
|
<Optimize>false</Optimize>
|
||||||
|
<OutputPath>bin\Debug\</OutputPath>
|
||||||
|
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||||
|
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||||
|
<DebugType>pdbonly</DebugType>
|
||||||
|
<Optimize>true</Optimize>
|
||||||
|
<OutputPath>bin\Release\</OutputPath>
|
||||||
|
<DefineConstants>TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="System" />
|
||||||
|
<Reference Include="System.Core" />
|
||||||
|
<Reference Include="System.Xml.Linq" />
|
||||||
|
<Reference Include="System.Data.DataSetExtensions" />
|
||||||
|
<Reference Include="Microsoft.CSharp" />
|
||||||
|
<Reference Include="System.Data" />
|
||||||
|
<Reference Include="System.Net.Http" />
|
||||||
|
<Reference Include="System.Xml" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="Program.cs" />
|
||||||
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="App.config" />
|
||||||
|
</ItemGroup>
|
||||||
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
|
</Project>
|
||||||
0
Crystal/First-Program/a.out
Normal file
0
Crystal/First-Program/a.out
Normal file
1
Crystal/First-Program/lib/discordcr.sha1
Normal file
1
Crystal/First-Program/lib/discordcr.sha1
Normal file
@@ -0,0 +1 @@
|
|||||||
|
5c722de5c25a6020466e80bc10d7904a0e30e7d5
|
||||||
14
Crystal/First-Program/lib/discordcr/.gitignore
vendored
Normal file
14
Crystal/First-Program/lib/discordcr/.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
.DS_Store
|
||||||
|
/doc/
|
||||||
|
/docs/
|
||||||
|
/libs/
|
||||||
|
/.crystal/
|
||||||
|
/.shards/
|
||||||
|
|
||||||
|
|
||||||
|
# Libraries don't need dependency lock
|
||||||
|
# Dependencies will be locked in application that uses them
|
||||||
|
/shard.lock
|
||||||
|
|
||||||
|
|
||||||
|
deploy_key
|
||||||
10
Crystal/First-Program/lib/discordcr/.travis.yml
Normal file
10
Crystal/First-Program/lib/discordcr/.travis.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
language: crystal
|
||||||
|
script:
|
||||||
|
- crystal spec
|
||||||
|
- crystal tool format --check
|
||||||
|
- find examples -name "*.cr" | xargs -L 1 crystal build --no-codegen
|
||||||
|
- bash ./deploy.sh
|
||||||
|
env:
|
||||||
|
global:
|
||||||
|
- ENCRYPTION_LABEL: "65183d8b3ae9"
|
||||||
|
- COMMIT_AUTHOR_EMAIL: "blactbt@live.de"
|
||||||
21
Crystal/First-Program/lib/discordcr/LICENSE
Normal file
21
Crystal/First-Program/lib/discordcr/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2016 meew0
|
||||||
|
|
||||||
|
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.
|
||||||
88
Crystal/First-Program/lib/discordcr/README.md
Normal file
88
Crystal/First-Program/lib/discordcr/README.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
[](https://meew0.github.io/discordcr/doc/v0.1.0/)
|
||||||
|
|
||||||
|
# discordcr
|
||||||
|
|
||||||
|
(The "cr" stands for "creative name".)
|
||||||
|
|
||||||
|
discordcr is a minimalist [Discord](https://discordapp.com/) API library for
|
||||||
|
[Crystal](https://crystal-lang.org/), designed to be a complement to
|
||||||
|
[discordrb](https://github.com/meew0/discordrb) for users who want more control
|
||||||
|
and performance and who care less about ease-of-use.
|
||||||
|
|
||||||
|
discordcr isn't designed for beginners to the Discord API - while experience
|
||||||
|
with making bots isn't *required*, it's certainly recommended. If you feel
|
||||||
|
overwhelmed by the complex documentation, try
|
||||||
|
[discordrb](https://github.com/meew0/discordrb) first and then check back.
|
||||||
|
|
||||||
|
Unlike many other libs which handle a lot of stuff, like caching or resolving,
|
||||||
|
themselves automatically, discordcr requires the user to do such things
|
||||||
|
manually. It also doesn't provide any advanced abstractions for REST calls;
|
||||||
|
the methods perform the HTTP request with the given data but nothing else.
|
||||||
|
This means that the user has full control over them, but also full
|
||||||
|
responsibility. discordcr does not support user accounts; it may work but
|
||||||
|
likely doesn't.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Add this to your application's `shard.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
dependencies:
|
||||||
|
discordcr:
|
||||||
|
github: meew0/discordcr
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
An example bot can be found
|
||||||
|
[here](https://github.com/meew0/discordcr/blob/master/examples/ping.cr). More
|
||||||
|
examples will come in the future.
|
||||||
|
|
||||||
|
A short overview of library structure: the `Client` class includes the `REST`
|
||||||
|
module, which handles the REST parts of Discord's API; the `Client` itself
|
||||||
|
handles the gateway, i. e. the interactive parts such as receiving messages. It
|
||||||
|
is possible to use only the REST parts by never calling the `#run` method on a
|
||||||
|
`Client`, which is what does the actual gateway connection.
|
||||||
|
|
||||||
|
The example linked above has an example of an event (`on_message_create`) that
|
||||||
|
is called through the gateway, and of a REST call (`client.create_message`).
|
||||||
|
Other gateway events and REST calls work much in the same way - see the
|
||||||
|
documentation for what specific events and REST calls do.
|
||||||
|
|
||||||
|
Caching is done using a separate `Cache` class that needs to be added into
|
||||||
|
clients manually:
|
||||||
|
|
||||||
|
```cr
|
||||||
|
client = Discord::Client.new # ...
|
||||||
|
cache = Discord::Cache.new(client)
|
||||||
|
client.cache = cache
|
||||||
|
```
|
||||||
|
|
||||||
|
Resolution requests for objects can now be done on the `cache` object instead of
|
||||||
|
directly over REST, this ensures that if an object is needed more than once
|
||||||
|
there will still only be one request to Discord. (There may even be no request
|
||||||
|
at all, if the requested data has already been obtained over the gateway.)
|
||||||
|
An example of how to use the cache once it has been instantiated:
|
||||||
|
|
||||||
|
```cr
|
||||||
|
# Get the username of the user with ID 66237334693085184
|
||||||
|
user = cache.resolve_user(66237334693085184_u64)
|
||||||
|
user = cache.resolve_user(66237334693085184_u64) # won't do a request to Discord
|
||||||
|
puts user.username
|
||||||
|
```
|
||||||
|
|
||||||
|
Apart from this, API documentation is also available, at
|
||||||
|
https://meew0.github.io/discordcr/doc/v0.1.0/.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
1. Fork it (https://github.com/meew0/discordcr/fork)
|
||||||
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
||||||
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
||||||
|
4. Push to the branch (`git push origin my-new-feature`)
|
||||||
|
5. Create a new Pull Request
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
- [meew0](https://github.com/meew0) - creator, maintainer
|
||||||
|
- [RX14](https://github.com/RX14) - Crystal expert, maintainer
|
||||||
78
Crystal/First-Program/lib/discordcr/deploy.sh
Normal file
78
Crystal/First-Program/lib/discordcr/deploy.sh
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Script adapted from https://gist.github.com/domenic/ec8b0fc8ab45f39403dd
|
||||||
|
|
||||||
|
set -e # Exit with nonzero exit code if anything fails
|
||||||
|
|
||||||
|
SOURCE_BRANCH="master"
|
||||||
|
TARGET_BRANCH="gh-pages"
|
||||||
|
|
||||||
|
function doCompile {
|
||||||
|
crystal doc
|
||||||
|
}
|
||||||
|
|
||||||
|
# Pull requests and commits to other branches shouldn't try to deploy, just build to verify
|
||||||
|
if [ "$TRAVIS_PULL_REQUEST" != "false" ] || { [ "$TRAVIS_BRANCH" != "$SOURCE_BRANCH" ] && [ -z "$TRAVIS_TAG" ]; }; then
|
||||||
|
echo "Skipping deploy; just doing a build."
|
||||||
|
doCompile
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$TRAVIS_TAG" ]; then
|
||||||
|
SOURCE_BRANCH=$TRAVIS_TAG
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Save some useful information
|
||||||
|
REPO=`git config remote.origin.url`
|
||||||
|
SSH_REPO=${REPO/https:\/\/github.com\//git@github.com:}
|
||||||
|
SHA=`git rev-parse --verify HEAD`
|
||||||
|
|
||||||
|
# Clone the existing gh-pages for this repo into out/
|
||||||
|
# Create a new empty branch if gh-pages doesn't exist yet (should only happen on first deply)
|
||||||
|
git clone $REPO out
|
||||||
|
cd out
|
||||||
|
git checkout $TARGET_BRANCH || git checkout --orphan $TARGET_BRANCH
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
mkdir -p out/doc/$SOURCE_BRANCH
|
||||||
|
|
||||||
|
# Clean out existing contents
|
||||||
|
rm -rf out/doc/$SOURCE_BRANCH/**/* || exit 0
|
||||||
|
|
||||||
|
# Run our compile script
|
||||||
|
doCompile
|
||||||
|
|
||||||
|
# Move results
|
||||||
|
mv docs/* out/doc/$SOURCE_BRANCH/
|
||||||
|
|
||||||
|
# Now let's go have some fun with the cloned repo
|
||||||
|
cd out
|
||||||
|
git config user.name "Travis CI"
|
||||||
|
git config user.email "$COMMIT_AUTHOR_EMAIL"
|
||||||
|
|
||||||
|
git add -N doc/$SOURCE_BRANCH
|
||||||
|
|
||||||
|
# If there are no changes to the compiled out (e.g. this is a README update) then just bail.
|
||||||
|
DIFF_RESULT=`git diff`
|
||||||
|
if [ -z "$DIFF_RESULT" ]; then
|
||||||
|
echo "No changes to the output on this push; exiting."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Commit the "changes", i.e. the new version.
|
||||||
|
# The delta will show diffs between new and old versions.
|
||||||
|
git add .
|
||||||
|
git commit -m "Deploy to GitHub Pages: ${SHA}"
|
||||||
|
|
||||||
|
# Get the deploy key by using Travis's stored variables to decrypt deploy_key.enc
|
||||||
|
ENCRYPTED_KEY_VAR="encrypted_${ENCRYPTION_LABEL}_key"
|
||||||
|
ENCRYPTED_IV_VAR="encrypted_${ENCRYPTION_LABEL}_iv"
|
||||||
|
ENCRYPTED_KEY=${!ENCRYPTED_KEY_VAR}
|
||||||
|
ENCRYPTED_IV=${!ENCRYPTED_IV_VAR}
|
||||||
|
openssl aes-256-cbc -K $ENCRYPTED_KEY -iv $ENCRYPTED_IV -in deploy_key.enc -out deploy_key -d
|
||||||
|
chmod 600 deploy_key
|
||||||
|
eval `ssh-agent -s`
|
||||||
|
ssh-add deploy_key
|
||||||
|
|
||||||
|
# Now that we're all set up, we can push.
|
||||||
|
git push $SSH_REPO $TARGET_BRANCH
|
||||||
BIN
Crystal/First-Program/lib/discordcr/deploy_key.enc
Normal file
BIN
Crystal/First-Program/lib/discordcr/deploy_key.enc
Normal file
Binary file not shown.
@@ -0,0 +1,50 @@
|
|||||||
|
# This example demonstrates usage of `Discord::Mention.parse` to parse
|
||||||
|
# and handle different kinds of mentions appearing in a message.
|
||||||
|
|
||||||
|
require "../src/discordcr"
|
||||||
|
|
||||||
|
# Make sure to replace this fake data with actual data when running.
|
||||||
|
client = Discord::Client.new(token: "Bot MjI5NDU5NjgxOTU1NjUyMzM3.Cpnz31.GQ7K9xwZtvC40y8MPY3eTqjEIXm")
|
||||||
|
|
||||||
|
client.on_message_create do |payload|
|
||||||
|
next unless payload.content.starts_with?("parse:")
|
||||||
|
|
||||||
|
mentions = String.build do |string|
|
||||||
|
index = 0
|
||||||
|
Discord::Mention.parse(payload.content) do |mention|
|
||||||
|
index += 1
|
||||||
|
string << "`[" << index << " @ " << mention.start << "]` "
|
||||||
|
case mention
|
||||||
|
when Discord::Mention::User
|
||||||
|
string.puts "**User:** #{mention.id}"
|
||||||
|
when Discord::Mention::Role
|
||||||
|
string.puts "**Role:** #{mention.id}"
|
||||||
|
when Discord::Mention::Channel
|
||||||
|
string.puts "**Channel:** #{mention.id}"
|
||||||
|
when Discord::Mention::Emoji
|
||||||
|
string << "**Emoji:** #{mention.name} #{mention.id}"
|
||||||
|
string << " (animated)" if mention.animated
|
||||||
|
string.puts
|
||||||
|
when Discord::Mention::Everyone
|
||||||
|
string.puts "**Everyone**"
|
||||||
|
when Discord::Mention::Here
|
||||||
|
string.puts "**Here**"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
mentions = "no mentions found in your message" if mentions.empty?
|
||||||
|
|
||||||
|
begin
|
||||||
|
client.create_message(
|
||||||
|
payload.channel_id,
|
||||||
|
mentions)
|
||||||
|
rescue ex
|
||||||
|
client.create_message(
|
||||||
|
payload.channel_id,
|
||||||
|
"`#{ex.inspect}`")
|
||||||
|
raise ex
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
client.run
|
||||||
35
Crystal/First-Program/lib/discordcr/examples/multicommand.cr
Normal file
35
Crystal/First-Program/lib/discordcr/examples/multicommand.cr
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# multicommand.cr is an example that uses a simple command "dispatcher"
|
||||||
|
# via a case statement.
|
||||||
|
# This example features a few commands:
|
||||||
|
# » !help ==> sends a dm (direct message) to the user
|
||||||
|
# with information
|
||||||
|
# » !about ==> prints about information in a code block
|
||||||
|
# » !echo <args> ==> echos args
|
||||||
|
# » !date ==> prints the current date
|
||||||
|
|
||||||
|
require "../src/discordcr"
|
||||||
|
|
||||||
|
# Make sure to replace this fake data with actual data when running.
|
||||||
|
client = Discord::Client.new(token: "Bot MjI5NDU5NjgxOTU1NjUyMzM3.Cpnz31.GQ7K9xwZtvC40y8MPY3eTqjEIXm", client_id: 229459681955652337_u64)
|
||||||
|
|
||||||
|
# Command Prefix
|
||||||
|
PREFIX = "!"
|
||||||
|
|
||||||
|
client.on_message_create do |payload|
|
||||||
|
command = payload.content
|
||||||
|
case command
|
||||||
|
when PREFIX + "help"
|
||||||
|
client.create_message(client.create_dm(payload.author.id).id, "Help is on the way!")
|
||||||
|
when PREFIX + "about"
|
||||||
|
block = "```\nBot developed by discordcr\n```"
|
||||||
|
client.create_message(payload.channel_id, block)
|
||||||
|
when .starts_with? PREFIX + "echo"
|
||||||
|
# !echo is a good example of a command with arguments (suffix)
|
||||||
|
suffix = command.split(' ')[1..-1].join(" ")
|
||||||
|
client.create_message(payload.channel_id, suffix)
|
||||||
|
when PREFIX + "date"
|
||||||
|
client.create_message(payload.channel_id, Time.now.to_s("%D"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
client.run
|
||||||
14
Crystal/First-Program/lib/discordcr/examples/ping.cr
Normal file
14
Crystal/First-Program/lib/discordcr/examples/ping.cr
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# This simple example bot replies to every "!ping" message with "Pong!".
|
||||||
|
|
||||||
|
require "../src/discordcr"
|
||||||
|
|
||||||
|
# Make sure to replace this fake data with actual data when running.
|
||||||
|
client = Discord::Client.new(token: "Bot MjI5NDU5NjgxOTU1NjUyMzM3.Cpnz31.GQ7K9xwZtvC40y8MPY3eTqjEIXm", client_id: 229459681955652337_u64)
|
||||||
|
|
||||||
|
client.on_message_create do |payload|
|
||||||
|
if payload.content.starts_with? "!ping"
|
||||||
|
client.create_message(payload.channel_id, "Pong!")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
client.run
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# This example is nearly the same as the normal ping example, but rather than simply
|
||||||
|
# responding with "Pong!", it also responds with the time it took to send the message.
|
||||||
|
|
||||||
|
require "../src/discordcr"
|
||||||
|
|
||||||
|
# Make sure to replace this fake data with actual data when running.
|
||||||
|
client = Discord::Client.new(token: "Bot MjI5NDU5NjgxOTU1NjUyMzM3.Cpnz31.GQ7K9xwZtvC40y8MPY3eTqjEIXm", client_id: 229459681955652337_u64)
|
||||||
|
|
||||||
|
client.on_message_create do |payload|
|
||||||
|
if payload.content.starts_with? "!ping"
|
||||||
|
# We first create a new Message, and then we check how long it took to send the message by comparing it to the current time
|
||||||
|
m = client.create_message(payload.channel_id, "Pong!")
|
||||||
|
time = Time.utc_now - payload.timestamp
|
||||||
|
client.edit_message(m.channel_id, m.id, "Pong! Time taken: #{time.total_milliseconds} ms.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
client.run
|
||||||
130
Crystal/First-Program/lib/discordcr/examples/voice_send.cr
Normal file
130
Crystal/First-Program/lib/discordcr/examples/voice_send.cr
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
# This is a simple music bot that can connect to a voice channel and play back
|
||||||
|
# some music in DCA format. It demonstrates how to use VoiceClient and
|
||||||
|
# DCAParser.
|
||||||
|
#
|
||||||
|
# For more information on the DCA file format, see
|
||||||
|
# https://github.com/bwmarrin/dca.
|
||||||
|
|
||||||
|
require "../src/discordcr"
|
||||||
|
|
||||||
|
# Make sure to replace this fake data with actual data when running.
|
||||||
|
client = Discord::Client.new(token: "Bot MjI5NDU5NjgxOTU1NjUyMzM3.Cpnz31.GQ7K9xwZtvC40y8MPY3eTqjEIXm", client_id: 229459681955652337_u64)
|
||||||
|
|
||||||
|
# ID of the current user, required to create a voice client
|
||||||
|
current_user_id = nil
|
||||||
|
|
||||||
|
# The ID of the (text) channel in which the connect command was run, so the
|
||||||
|
# "Voice connected." message is sent to the correct channel
|
||||||
|
connect_channel_id = nil
|
||||||
|
|
||||||
|
# Where the created voice client will eventually be stored
|
||||||
|
voice_client = nil
|
||||||
|
|
||||||
|
client.on_ready do |payload|
|
||||||
|
current_user_id = payload.user.id
|
||||||
|
end
|
||||||
|
|
||||||
|
client.on_message_create do |payload|
|
||||||
|
if payload.content.starts_with? "!connect "
|
||||||
|
# Used as:
|
||||||
|
# !connect <guild ID> <channel ID>
|
||||||
|
|
||||||
|
# Parse the command arguments
|
||||||
|
ids = payload.content[9..-1].split(' ').map(&.to_u64)
|
||||||
|
|
||||||
|
client.create_message(payload.channel_id, "Connecting...")
|
||||||
|
connect_channel_id = payload.channel_id
|
||||||
|
client.voice_state_update(ids[0].to_u64, ids[1].to_u64, false, false)
|
||||||
|
elsif payload.content.starts_with? "!play_dca "
|
||||||
|
# Used as:
|
||||||
|
# !play_dca <filename>
|
||||||
|
#
|
||||||
|
# Make sure the DCA file you play back is valid according to the spec
|
||||||
|
# (including metadata), otherwise playback will fail.
|
||||||
|
|
||||||
|
unless voice_client
|
||||||
|
client.create_message(payload.channel_id, "Voice client is nil!")
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
filename = payload.content[10..-1]
|
||||||
|
file = File.open(filename)
|
||||||
|
|
||||||
|
# The DCAParser class handles parsing of the DCA file. It doesn't do any
|
||||||
|
# sending of audio data to Discord itself – that has to be done by
|
||||||
|
# VoiceClient.
|
||||||
|
parser = Discord::DCAParser.new(file)
|
||||||
|
|
||||||
|
# A proper DCA(1) file contains metadata, which is exposed by DCAParser.
|
||||||
|
# This metadata may be of interest, so here is some example code that uses
|
||||||
|
# it.
|
||||||
|
if metadata = parser.metadata
|
||||||
|
tool = metadata.dca.tool
|
||||||
|
client.create_message(payload.channel_id, "DCA file was created by #{tool.name}, version #{tool.version}.")
|
||||||
|
|
||||||
|
if info = metadata.info
|
||||||
|
client.create_message(payload.channel_id, "Song info: #{info.title} by #{info.artist}.") if info.title && info.artist
|
||||||
|
end
|
||||||
|
else
|
||||||
|
client.create_message(payload.channel_id, "DCA file metadata is invalid!")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Set the bot as speaking (green circle). This is important and has to be
|
||||||
|
# done at least once in every voice connection, otherwise the Discord client
|
||||||
|
# will not know who the packets we're sending belongs to.
|
||||||
|
voice_client.not_nil!.send_speaking(true)
|
||||||
|
|
||||||
|
client.create_message(payload.channel_id, "Playing DCA file `#{filename}`.")
|
||||||
|
|
||||||
|
# For smooth audio streams Discord requires one packet every
|
||||||
|
# 20 milliseconds. The `every` method measures the time it takes to run the
|
||||||
|
# block and then sleeps 20 milliseconds minus that time before moving on to
|
||||||
|
# the next iteration, ensuring accurate timing.
|
||||||
|
#
|
||||||
|
# When simply reading from DCA, the time it takes to read, process and
|
||||||
|
# send the frame is small enough that `every` doesn't make much of a
|
||||||
|
# difference (in fact, some users report that it actually makes things
|
||||||
|
# worse). If the processing time is not negligibly slow because you're
|
||||||
|
# doing something else than DCA parsing, or because you're reading from a
|
||||||
|
# slow source, or for any other reason, then it is recommended to use
|
||||||
|
# `every`. Otherwise, simply using a loop and `sleep`ing `20.milliseconds`
|
||||||
|
# each time may suffice.
|
||||||
|
Discord.every(20.milliseconds) do
|
||||||
|
frame = parser.next_frame(reuse_buffer: true)
|
||||||
|
break unless frame
|
||||||
|
|
||||||
|
# Perform the actual sending of the frame to Discord.
|
||||||
|
voice_client.not_nil!.play_opus(frame)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Alternatively, the above code can be realised as the following:
|
||||||
|
#
|
||||||
|
# parser.parse do |frame|
|
||||||
|
# Discord.timed_run(20.milliseconds) do
|
||||||
|
# voice_client.not_nil!.play_opus(frame)
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# (The `parse` method reads the frames consecutively and passes them to the
|
||||||
|
# block.)
|
||||||
|
|
||||||
|
file.close
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# The VOICE_SERVER_UPDATE dispatch is sent by Discord once the op4 packet sent
|
||||||
|
# by voice_state_update has been processed. It tells the client the endpoint
|
||||||
|
# to connect to.
|
||||||
|
client.on_voice_server_update do |payload|
|
||||||
|
begin
|
||||||
|
vc = voice_client = Discord::VoiceClient.new(payload, client.session.not_nil!, current_user_id.not_nil!)
|
||||||
|
vc.on_ready do
|
||||||
|
client.create_message(connect_channel_id.not_nil!, "Voice connected.")
|
||||||
|
end
|
||||||
|
vc.run
|
||||||
|
rescue e
|
||||||
|
e.inspect_with_backtrace(STDOUT)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
client.run
|
||||||
17
Crystal/First-Program/lib/discordcr/examples/welcome.cr
Normal file
17
Crystal/First-Program/lib/discordcr/examples/welcome.cr
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# This simple example bot creates a message whenever a new user joins the server
|
||||||
|
|
||||||
|
require "../src/discordcr"
|
||||||
|
|
||||||
|
# Make sure to replace this fake data with actual data when running.
|
||||||
|
client = Discord::Client.new(token: "Bot MjI5NDU5NjgxOTU1NjUyMzM3.Cpnz31.GQ7K9xwZtvC40y8MPY3eTqjEIXm", client_id: 229459681955652337_u64)
|
||||||
|
cache = Discord::Cache.new(client)
|
||||||
|
client.cache = cache
|
||||||
|
|
||||||
|
client.on_guild_member_add do |payload|
|
||||||
|
# get the guild/server information
|
||||||
|
guild = cache.resolve_guild(payload.guild_id)
|
||||||
|
|
||||||
|
client.create_message(guild.id, "Please welcome <@#{payload.user.id}> to #{guild.name}.")
|
||||||
|
end
|
||||||
|
|
||||||
|
client.run
|
||||||
7
Crystal/First-Program/lib/discordcr/shard.yml
Normal file
7
Crystal/First-Program/lib/discordcr/shard.yml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
name: discordcr
|
||||||
|
version: 0.3.0
|
||||||
|
|
||||||
|
authors:
|
||||||
|
- meew0 <blactbt@live.de>
|
||||||
|
|
||||||
|
license: MIT
|
||||||
171
Crystal/First-Program/lib/discordcr/spec/discordcr_spec.cr
Normal file
171
Crystal/First-Program/lib/discordcr/spec/discordcr_spec.cr
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
require "yaml"
|
||||||
|
require "./spec_helper"
|
||||||
|
|
||||||
|
struct StructWithSnowflake
|
||||||
|
JSON.mapping(
|
||||||
|
data: {type: UInt64, converter: Discord::SnowflakeConverter}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct StructWithMaybeSnowflake
|
||||||
|
JSON.mapping(
|
||||||
|
data: {type: UInt64?, converter: Discord::MaybeSnowflakeConverter}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct StructWithSnowflakeArray
|
||||||
|
JSON.mapping(
|
||||||
|
data: {type: Array(UInt64), converter: Discord::SnowflakeArrayConverter}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct StructWithTime
|
||||||
|
JSON.mapping(
|
||||||
|
data: {type: Time, converter: Discord::TimestampConverter}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct StructWithMessageType
|
||||||
|
JSON.mapping(
|
||||||
|
data: {type: Discord::MessageType, converter: Discord::MessageTypeConverter}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct StructWithChannelType
|
||||||
|
JSON.mapping(
|
||||||
|
data: {type: Discord::ChannelType, converter: Discord::ChannelTypeConverter}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe Discord do
|
||||||
|
describe "VERSION" do
|
||||||
|
it "matches shards.yml" do
|
||||||
|
version = YAML.parse(File.read(File.join(__DIR__, "..", "shard.yml")))["version"].as_s
|
||||||
|
version.should eq(Discord::VERSION)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe Discord::TimestampConverter do
|
||||||
|
it "parses a time with floating point accuracy" do
|
||||||
|
json = %({"data":"2017-11-16T13:09:18.291000+00:00"})
|
||||||
|
|
||||||
|
obj = StructWithTime.from_json(json)
|
||||||
|
obj.data.should be_a Time
|
||||||
|
end
|
||||||
|
|
||||||
|
it "parses a time without floating point accuracy" do
|
||||||
|
json = %({"data":"2017-11-15T02:23:35+00:00"})
|
||||||
|
|
||||||
|
obj = StructWithTime.from_json(json)
|
||||||
|
obj.data.should be_a Time
|
||||||
|
end
|
||||||
|
|
||||||
|
it "serializes" do
|
||||||
|
json = %({"data":"2017-11-16T13:09:18.291000+00:00"})
|
||||||
|
obj = StructWithTime.from_json(json)
|
||||||
|
obj.to_json.should eq json
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe Discord::SnowflakeConverter do
|
||||||
|
it "converts a string to u64" do
|
||||||
|
json = %({"data":"10000000000"})
|
||||||
|
|
||||||
|
obj = StructWithSnowflake.from_json(json)
|
||||||
|
obj.data.should eq 10000000000
|
||||||
|
obj.data.should be_a UInt64
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe Discord::MaybeSnowflakeConverter do
|
||||||
|
it "converts a string to u64" do
|
||||||
|
json = %({"data":"10000000000"})
|
||||||
|
|
||||||
|
obj = StructWithMaybeSnowflake.from_json(json)
|
||||||
|
obj.data.should eq 10000000000
|
||||||
|
obj.data.should be_a UInt64
|
||||||
|
end
|
||||||
|
|
||||||
|
it "converts null to nil" do
|
||||||
|
json = %({"data":null})
|
||||||
|
|
||||||
|
obj = StructWithMaybeSnowflake.from_json(json)
|
||||||
|
obj.data.should eq nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe Discord::SnowflakeArrayConverter do
|
||||||
|
it "converts an array of strings to u64s" do
|
||||||
|
json = %({"data":["1", "2", "10000000000"]})
|
||||||
|
|
||||||
|
obj = StructWithSnowflakeArray.from_json(json)
|
||||||
|
obj.data.should be_a Array(UInt64)
|
||||||
|
obj.data[0].should eq 1
|
||||||
|
obj.data[1].should eq 2
|
||||||
|
obj.data[2].should eq 10000000000
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe Discord::REST::ModifyChannelPositionPayload do
|
||||||
|
describe "#to_json" do
|
||||||
|
context "parent_id is ChannelParent::Unchanged" do
|
||||||
|
it "doesn't emit parent_id" do
|
||||||
|
payload = {Discord::REST::ModifyChannelPositionPayload.new(0_u64, 0, Discord::REST::ChannelParent::Unchanged, true)}
|
||||||
|
payload.to_json.should eq %([{"id":"0","position":0,"lock_permissions":true}])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "parent_id is ChannelParent::None" do
|
||||||
|
it "emits null for parent_id" do
|
||||||
|
payload = {Discord::REST::ModifyChannelPositionPayload.new(0_u64, 0, Discord::REST::ChannelParent::None, true)}
|
||||||
|
payload.to_json.should eq %([{"id":"0","position":0,"parent_id":null,"lock_permissions":true}])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe Discord::MessageTypeConverter do
|
||||||
|
it "converts an integer into a MessageType" do
|
||||||
|
json = %({"data": 0})
|
||||||
|
|
||||||
|
obj = StructWithMessageType.from_json(json)
|
||||||
|
obj.data.should eq Discord::MessageType::Default
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with an invalid json value" do
|
||||||
|
it "raises" do
|
||||||
|
json = %({"data":"foo"})
|
||||||
|
|
||||||
|
expect_raises(Exception, %(Unexpected message type value: "foo")) do
|
||||||
|
StructWithMessageType.from_json(json)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe Discord::WebSocket::Packet do
|
||||||
|
it "inspects" do
|
||||||
|
packet = Discord::WebSocket::Packet.new(0_i64, 1_i64, IO::Memory.new("foo"), "test")
|
||||||
|
packet.inspect.should eq %(Discord::WebSocket::Packet(@opcode=0_i64 @sequence=1_i64 @data="foo" @event_type="test"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe Discord::ChannelTypeConverter do
|
||||||
|
it "converts an integer into a ChannelType" do
|
||||||
|
json = %({"data": 0})
|
||||||
|
|
||||||
|
obj = StructWithChannelType.from_json(json)
|
||||||
|
obj.data.should eq Discord::ChannelType::GuildText
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with an invalid json value" do
|
||||||
|
it "raises" do
|
||||||
|
json = %({"data":"foo"})
|
||||||
|
|
||||||
|
expect_raises(Exception, %(Unexpected channel type value: "foo")) do
|
||||||
|
StructWithChannelType.from_json(json)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
50
Crystal/First-Program/lib/discordcr/spec/mention_spec.cr
Normal file
50
Crystal/First-Program/lib/discordcr/spec/mention_spec.cr
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
require "./spec_helper"
|
||||||
|
|
||||||
|
def it_parses_message(string, into expected)
|
||||||
|
it "parses #{string.inspect} into #{expected}" do
|
||||||
|
parsed = Discord::Mention.parse(string)
|
||||||
|
parsed.should eq expected
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe Discord::Mention do
|
||||||
|
describe ".parse" do
|
||||||
|
it_parses_message(
|
||||||
|
"<@123><@!456>",
|
||||||
|
into: [
|
||||||
|
Discord::Mention::User.new(123_u64, 0, 6),
|
||||||
|
Discord::Mention::User.new(456_u64, 6, 7),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
it_parses_message(
|
||||||
|
"<@&123>",
|
||||||
|
into: [Discord::Mention::Role.new(123_u64, 0, 6)])
|
||||||
|
|
||||||
|
it_parses_message(
|
||||||
|
"<#123>",
|
||||||
|
into: [Discord::Mention::Channel.new(123_u64, 0, 6)])
|
||||||
|
|
||||||
|
it_parses_message(
|
||||||
|
"<:foo:123><a:bar:456>",
|
||||||
|
into: [
|
||||||
|
Discord::Mention::Emoji.new(false, "foo", 123_u64, 0, 10),
|
||||||
|
Discord::Mention::Emoji.new(true, "bar", 456_u64, 10, 11),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
it_parses_message(
|
||||||
|
"@everyone@here",
|
||||||
|
into: [
|
||||||
|
Discord::Mention::Everyone.new(0),
|
||||||
|
Discord::Mention::Here.new(9),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
context "with invalid mentions" do
|
||||||
|
it_parses_message(
|
||||||
|
"<<@123<@?123><#123<:foo:123<b:foo:123><@abc><@!abc>",
|
||||||
|
into: [] of Discord::Mention)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
10
Crystal/First-Program/lib/discordcr/spec/rest_spec.cr
Normal file
10
Crystal/First-Program/lib/discordcr/spec/rest_spec.cr
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
require "./spec_helper"
|
||||||
|
|
||||||
|
describe Discord::REST do
|
||||||
|
describe "#encode_tuple" do
|
||||||
|
it "doesn't emit null values" do
|
||||||
|
client = Discord::Client.new("foo", 0_u64)
|
||||||
|
client.encode_tuple(foo: ["bar", 1, 2], baz: nil).should eq(%({"foo":["bar",1,2]}))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
2
Crystal/First-Program/lib/discordcr/spec/spec_helper.cr
Normal file
2
Crystal/First-Program/lib/discordcr/spec/spec_helper.cr
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
require "spec"
|
||||||
|
require "../src/discordcr"
|
||||||
5
Crystal/First-Program/lib/discordcr/src/discordcr.cr
Normal file
5
Crystal/First-Program/lib/discordcr/src/discordcr.cr
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
require "./discordcr/*"
|
||||||
|
|
||||||
|
module Discord
|
||||||
|
# TODO Put your code here
|
||||||
|
end
|
||||||
252
Crystal/First-Program/lib/discordcr/src/discordcr/cache.cr
Normal file
252
Crystal/First-Program/lib/discordcr/src/discordcr/cache.cr
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
require "./mappings/*"
|
||||||
|
|
||||||
|
module Discord
|
||||||
|
# A cache is a utility class that stores various kinds of Discord objects,
|
||||||
|
# like `User`s, `Role`s etc. Its purpose is to reduce both the load on
|
||||||
|
# Discord's servers and reduce the latency caused by having to do an API call.
|
||||||
|
# It is recommended to use caching for bots that interact heavily with
|
||||||
|
# Discord-provided data, like for example administration bots, as opposed to
|
||||||
|
# bots that only interact by sending and receiving messages. For that latter
|
||||||
|
# kind, caching is usually even counter-productive as it only unnecessarily
|
||||||
|
# increases memory usage.
|
||||||
|
#
|
||||||
|
# Caching can either be used standalone, in a purely REST-based way:
|
||||||
|
# ```
|
||||||
|
# client = Discord::Client.new(token: "Bot token", client_id: 123_u64)
|
||||||
|
# cache = Discord::Cache.new(client)
|
||||||
|
#
|
||||||
|
# puts cache.resolve_user(66237334693085184) # will perform API call
|
||||||
|
# puts cache.resolve_user(66237334693085184) # will not perform an API call, as the data is now cached
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# It can also be integrated more deeply into a `Client` (specifically one that
|
||||||
|
# uses a gateway connection) to reduce cache misses even more by automatically
|
||||||
|
# caching data received over the gateway:
|
||||||
|
# ```
|
||||||
|
# client = Discord::Client.new(token: "Bot token", client_id: 123_u64)
|
||||||
|
# cache = Discord::Cache.new(client)
|
||||||
|
# client.cache = cache # Integrate the cache into the client
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# Note that if a cache is *not* used this way, its data will slowly go out of
|
||||||
|
# sync with Discord, and unless it is used in an environment with few changes
|
||||||
|
# likely to occur, a client without a gateway connection should probably
|
||||||
|
# refrain from caching at all.
|
||||||
|
class Cache
|
||||||
|
# A map of cached users. These aren't necessarily all the users in servers
|
||||||
|
# the bot has access to, but rather all the users that have been seen by
|
||||||
|
# the bot in the past (and haven't been deleted by means of `delete_user`).
|
||||||
|
getter users
|
||||||
|
|
||||||
|
# A map of cached channels, i. e. all channels on all servers the bot is on,
|
||||||
|
# as well as all DM channels.
|
||||||
|
getter channels
|
||||||
|
|
||||||
|
# A map of guilds (servers) the bot is on. Doesn't ignore guilds temporarily
|
||||||
|
# deleted due to an outage; so if an outage is going on right now the
|
||||||
|
# affected guilds would be missing here too.
|
||||||
|
getter guilds
|
||||||
|
|
||||||
|
# A double map of members on servers, represented as {guild ID => {user ID
|
||||||
|
# => member}}. Will only contain previously and currently online members as
|
||||||
|
# well as all members that have been chunked (see
|
||||||
|
# `Client#request_guild_members`).
|
||||||
|
getter members
|
||||||
|
|
||||||
|
# A map of all roles on servers the bot is on. Does not discriminate by
|
||||||
|
# guild, as role IDs are unique even across guilds.
|
||||||
|
getter roles
|
||||||
|
|
||||||
|
# Mapping of users to the respective DM channels the bot has open with them,
|
||||||
|
# represented as {user ID => channel ID}.
|
||||||
|
getter dm_channels
|
||||||
|
|
||||||
|
# Mapping of guilds to the roles on them, represented as {guild ID =>
|
||||||
|
# [role IDs]}.
|
||||||
|
getter guild_roles
|
||||||
|
|
||||||
|
# Mapping of guilds to the channels on them, represented as {guild ID =>
|
||||||
|
# [channel IDs]}.
|
||||||
|
getter guild_channels
|
||||||
|
|
||||||
|
# Creates a new cache with a *client* that requests (in case of cache
|
||||||
|
# misses) should be done on.
|
||||||
|
def initialize(@client : Client)
|
||||||
|
@users = Hash(UInt64, User).new
|
||||||
|
@channels = Hash(UInt64, Channel).new
|
||||||
|
@guilds = Hash(UInt64, Guild).new
|
||||||
|
@members = Hash(UInt64, Hash(UInt64, GuildMember)).new
|
||||||
|
@roles = Hash(UInt64, Role).new
|
||||||
|
|
||||||
|
@dm_channels = Hash(UInt64, UInt64).new
|
||||||
|
|
||||||
|
@guild_roles = Hash(UInt64, Array(UInt64)).new
|
||||||
|
@guild_channels = Hash(UInt64, Array(UInt64)).new
|
||||||
|
end
|
||||||
|
|
||||||
|
# Resolves a user by its *ID*. If the requested object is not cached, it
|
||||||
|
# will do an API call.
|
||||||
|
def resolve_user(id : UInt64) : User
|
||||||
|
@users.fetch(id) { @users[id] = @client.get_user(id) }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Resolves a channel by its *ID*. If the requested object is not cached, it
|
||||||
|
# will do an API call.
|
||||||
|
def resolve_channel(id : UInt64) : Channel
|
||||||
|
@channels.fetch(id) { @channels[id] = @client.get_channel(id) }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Resolves a guild by its *ID*. If the requested object is not cached, it
|
||||||
|
# will do an API call.
|
||||||
|
def resolve_guild(id : UInt64) : Guild
|
||||||
|
@guilds.fetch(id) { @guilds[id] = @client.get_guild(id) }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Resolves a member by the *guild_id* of the guild the member is on, and the
|
||||||
|
# *user_id* of the member itself. An API request will be performed if the
|
||||||
|
# object is not cached.
|
||||||
|
def resolve_member(guild_id : UInt64, user_id : UInt64) : GuildMember
|
||||||
|
local_members = @members[guild_id] ||= Hash(UInt64, GuildMember).new
|
||||||
|
local_members.fetch(user_id) { local_members[user_id] = @client.get_guild_member(guild_id, user_id) }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Resolves a role by its *ID*. No API request will be performed if the role
|
||||||
|
# is not cached, because there is no endpoint for individual roles; however
|
||||||
|
# all roles should be cached at all times so it won't be a problem.
|
||||||
|
def resolve_role(id : UInt64) : Role
|
||||||
|
@roles[id] # There is no endpoint for getting an individual role, so we will have to ignore that case for now.
|
||||||
|
end
|
||||||
|
|
||||||
|
# Resolves the ID of a DM channel with a particular user by the recipient's
|
||||||
|
# *recipient_id*. If there is no such channel cached, one will be created.
|
||||||
|
def resolve_dm_channel(recipient_id : UInt64) : UInt64
|
||||||
|
@dm_channels.fetch(recipient_id) do
|
||||||
|
channel = @client.create_dm(recipient_id)
|
||||||
|
cache(Channel.new(channel))
|
||||||
|
channel.id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Resolves the current user's profile. Requires no parameters since the
|
||||||
|
# endpoint has none either. If there is a gateway connection this should
|
||||||
|
# always be cached.
|
||||||
|
def resolve_current_user : User
|
||||||
|
@current_user ||= @client.get_current_user
|
||||||
|
end
|
||||||
|
|
||||||
|
# Deletes a user from the cache given its *ID*.
|
||||||
|
def delete_user(id : UInt64)
|
||||||
|
@users.delete(id)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Deletes a channel from the cache given its *ID*.
|
||||||
|
def delete_channel(id : UInt64)
|
||||||
|
@channels.delete(id)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Deletes a guild from the cache given its *ID*.
|
||||||
|
def delete_guild(id : UInt64)
|
||||||
|
@guilds.delete(id)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Deletes a member from the cache given its *user_id* and the *guild_id* it
|
||||||
|
# is on.
|
||||||
|
def delete_member(guild_id : UInt64, user_id : UInt64)
|
||||||
|
@members[guild_id]?.try &.delete(user_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Deletes a role from the cache given its *ID*.
|
||||||
|
def delete_role(id : UInt64)
|
||||||
|
@roles.delete(id)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Deletes a DM channel with a particular user given the *recipient_id*.
|
||||||
|
def delete_dm_channel(recipient_id : UInt64)
|
||||||
|
@dm_channels.delete(recipient_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Deletes the current user from the cache, if that will ever be necessary.
|
||||||
|
def delete_current_user
|
||||||
|
@current_user = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
# Adds a specific *user* to the cache.
|
||||||
|
def cache(user : User)
|
||||||
|
@users[user.id] = user
|
||||||
|
end
|
||||||
|
|
||||||
|
# Adds a specific *channel* to the cache.
|
||||||
|
def cache(channel : Channel)
|
||||||
|
@channels[channel.id] = channel
|
||||||
|
end
|
||||||
|
|
||||||
|
# Adds a specific *guild* to the cache.
|
||||||
|
def cache(guild : Guild)
|
||||||
|
@guilds[guild.id] = guild
|
||||||
|
end
|
||||||
|
|
||||||
|
# Adds a specific *member* to the cache, given the *guild_id* it is on.
|
||||||
|
def cache(member : GuildMember, guild_id : UInt64)
|
||||||
|
local_members = @members[guild_id] ||= Hash(UInt64, GuildMember).new
|
||||||
|
local_members[member.user.id] = member
|
||||||
|
end
|
||||||
|
|
||||||
|
# Adds a specific *role* to the cache.
|
||||||
|
def cache(role : Role)
|
||||||
|
@roles[role.id] = role
|
||||||
|
end
|
||||||
|
|
||||||
|
# Adds a particular DM channel to the cache, given the *channel_id* and the
|
||||||
|
# *recipient_id*.
|
||||||
|
def cache_dm_channel(channel_id : UInt64, recipient_id : UInt64)
|
||||||
|
@dm_channels[recipient_id] = channel_id
|
||||||
|
end
|
||||||
|
|
||||||
|
# Caches the current user.
|
||||||
|
def cache_current_user(@current_user : User); end
|
||||||
|
|
||||||
|
# Adds multiple *members* at once to the cache, given the *guild_id* they
|
||||||
|
# all share. This method exists to slightly reduce the overhead of
|
||||||
|
# processing chunks; outside of that it is likely not of much use.
|
||||||
|
def cache_multiple_members(members : Array(GuildMember), guild_id : UInt64)
|
||||||
|
local_members = @members[guild_id] ||= Hash(UInt64, GuildMember).new
|
||||||
|
members.each do |member|
|
||||||
|
local_members[member.user.id] = member
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns all roles of a guild, identified by its *guild_id*.
|
||||||
|
def guild_roles(guild_id : UInt64) : Array(UInt64)
|
||||||
|
@guild_roles[guild_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Marks a role, identified by the *role_id*, as belonging to a particular
|
||||||
|
# guild, identified by the *guild_id*.
|
||||||
|
def add_guild_role(guild_id : UInt64, role_id : UInt64)
|
||||||
|
local_roles = @guild_roles[guild_id] ||= [] of UInt64
|
||||||
|
local_roles << role_id
|
||||||
|
end
|
||||||
|
|
||||||
|
# Marks a role as not belonging to a particular guild anymore.
|
||||||
|
def remove_guild_role(guild_id : UInt64, role_id : UInt64)
|
||||||
|
@guild_roles[guild_id]?.try { |local_roles| local_roles.delete(role_id) }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns all channels of a guild, identified by its *guild_id*.
|
||||||
|
def guild_channels(guild_id : UInt64) : Array(UInt64)
|
||||||
|
@guild_channels[guild_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Marks a channel, identified by the *channel_id*, as belonging to a particular
|
||||||
|
# guild, identified by the *guild_id*.
|
||||||
|
def add_guild_channel(guild_id : UInt64, channel_id : UInt64)
|
||||||
|
local_channels = @guild_channels[guild_id] ||= [] of UInt64
|
||||||
|
local_channels << channel_id
|
||||||
|
end
|
||||||
|
|
||||||
|
# Marks a channel as not belonging to a particular guild anymore.
|
||||||
|
def remove_guild_channel(guild_id : UInt64, channel_id : UInt64)
|
||||||
|
@guild_channels[guild_id]?.try { |local_channels| local_channels.delete(channel_id) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
843
Crystal/First-Program/lib/discordcr/src/discordcr/client.cr
Normal file
843
Crystal/First-Program/lib/discordcr/src/discordcr/client.cr
Normal file
@@ -0,0 +1,843 @@
|
|||||||
|
require "json"
|
||||||
|
|
||||||
|
require "./logger"
|
||||||
|
require "./rest"
|
||||||
|
require "./cache"
|
||||||
|
|
||||||
|
module Discord
|
||||||
|
# The basic client class that is used to connect to Discord, send REST
|
||||||
|
# requests, or send or receive gateway messages. It is required for doing any
|
||||||
|
# sort of interaction with Discord.
|
||||||
|
#
|
||||||
|
# A new simple client that does nothing yet can be created like this:
|
||||||
|
# ```
|
||||||
|
# client = Discord::Client.new(token: "Bot token", client_id: 123_u64)
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# With this client, REST requests can now be sent. (See the `Discord::REST`
|
||||||
|
# module.) A gateway connection can also be started using the `#run` method.
|
||||||
|
class Client
|
||||||
|
include REST
|
||||||
|
|
||||||
|
# If this is set to any `Cache`, the data in the cache will be updated as
|
||||||
|
# the client receives the corresponding gateway dispatches.
|
||||||
|
property cache : Cache?
|
||||||
|
|
||||||
|
# The internal *session* the client is currently using, necessary to create
|
||||||
|
# a voice client, for example
|
||||||
|
getter session : Gateway::Session?
|
||||||
|
|
||||||
|
# The internal websocket the client is currently using
|
||||||
|
getter websocket : Discord::WebSocket do
|
||||||
|
initialize_websocket
|
||||||
|
end
|
||||||
|
|
||||||
|
@backoff : Float64
|
||||||
|
|
||||||
|
# Default analytics properties sent in IDENTIFY
|
||||||
|
DEFAULT_PROPERTIES = Gateway::IdentifyProperties.new(
|
||||||
|
os: "Crystal",
|
||||||
|
browser: "discordcr",
|
||||||
|
device: "discordcr",
|
||||||
|
referrer: "",
|
||||||
|
referring_domain: ""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Creates a new bot with the given *token* and optionally the *client_id*.
|
||||||
|
# Both of these things can be found on a bot's application page; the token
|
||||||
|
# will need to be revealed using the "click to reveal" thing on the token
|
||||||
|
# (**not** the OAuth2 secret!)
|
||||||
|
#
|
||||||
|
# If the *shard* key is set, the gateway will operate in sharded mode. This
|
||||||
|
# means that this client's gateway connection will only receive packets from
|
||||||
|
# a part of the guilds the bot is connected to. See
|
||||||
|
# [here](https://discordapp.com/developers/docs/topics/gateway#sharding)
|
||||||
|
# for more information.
|
||||||
|
#
|
||||||
|
# The *large_threshold* defines the minimum member count that, if a guild
|
||||||
|
# has at least that many members, the client will only receive online
|
||||||
|
# members in GUILD_CREATE. The default value 100 is what the Discord client
|
||||||
|
# uses; the maximum value is 250. To get a list of offline members as well,
|
||||||
|
# the `#request_guild_members` method can be used.
|
||||||
|
#
|
||||||
|
# If *compress* is true, packets will be sent in a compressed manner.
|
||||||
|
# discordcr doesn't currently handle packet decompression, so until that is
|
||||||
|
# implemented, setting this to true will cause the client to fail to parse
|
||||||
|
# anything.
|
||||||
|
#
|
||||||
|
# The *properties* define what values are sent to Discord as analytics
|
||||||
|
# properties. It's not recommended to change these from the default values,
|
||||||
|
# but if you desire to do so, you can.
|
||||||
|
def initialize(@token : String, @client_id : UInt64? = nil,
|
||||||
|
@shard : Gateway::ShardKey? = nil,
|
||||||
|
@large_threshold : Int32 = 100,
|
||||||
|
@compress : Bool = false,
|
||||||
|
@properties : Gateway::IdentifyProperties = DEFAULT_PROPERTIES)
|
||||||
|
@backoff = 1.0
|
||||||
|
|
||||||
|
# Set some default value for the heartbeat interval. This should never
|
||||||
|
# actually be used as a delay between heartbeats because it will have an
|
||||||
|
# actual value before heartbeating starts.
|
||||||
|
@heartbeat_interval = 1000_u32
|
||||||
|
@send_heartbeats = false
|
||||||
|
|
||||||
|
# Initially, this flag is set to true so the client doesn't immediately
|
||||||
|
# try to reconnect at the next heartbeat.
|
||||||
|
@last_heartbeat_acked = true
|
||||||
|
|
||||||
|
# If the websocket is closed, whether we should immediately try and reconnect
|
||||||
|
@should_reconnect = true
|
||||||
|
|
||||||
|
setup_heartbeats
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns this client's ID as provided in its associated Oauth2 application.
|
||||||
|
# A getter for @client_id, this will make a REST call to obtain it
|
||||||
|
# if it was not provided in the initializer.
|
||||||
|
def client_id
|
||||||
|
@client_id ||= get_oauth2_application.id
|
||||||
|
end
|
||||||
|
|
||||||
|
# Connects this client to the gateway. This is required if the bot needs to
|
||||||
|
# do anything beyond making REST API calls. Calling this method will block
|
||||||
|
# execution until the bot is forcibly stopped.
|
||||||
|
def run
|
||||||
|
loop do
|
||||||
|
begin
|
||||||
|
websocket.run
|
||||||
|
rescue ex
|
||||||
|
LOGGER.error <<-LOG
|
||||||
|
Received exception from WebSocket#run:
|
||||||
|
#{ex.inspect_with_backtrace}
|
||||||
|
LOG
|
||||||
|
end
|
||||||
|
|
||||||
|
@send_heartbeats = false
|
||||||
|
@session.try &.suspend
|
||||||
|
|
||||||
|
break unless @should_reconnect
|
||||||
|
|
||||||
|
wait_for_reconnect
|
||||||
|
|
||||||
|
LOGGER.info "Reconnecting"
|
||||||
|
@websocket = initialize_websocket
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Closes the gateway connection permanently
|
||||||
|
def stop(message = nil)
|
||||||
|
@should_reconnect = false
|
||||||
|
websocket.close(message)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Separate method to wait an ever-increasing amount of time before reconnecting after being disconnected in an
|
||||||
|
# unexpected way
|
||||||
|
def wait_for_reconnect
|
||||||
|
# Wait before reconnecting so we don't spam Discord's servers.
|
||||||
|
LOGGER.debug "Attempting to reconnect in #{@backoff} seconds."
|
||||||
|
sleep @backoff.seconds
|
||||||
|
|
||||||
|
# Calculate new backoff
|
||||||
|
@backoff = 1.0 if @backoff < 1.0
|
||||||
|
@backoff *= 1.5
|
||||||
|
@backoff = 115 + (rand * 10) if @backoff > 120 # Cap the backoff at 120 seconds and then add some random jitter
|
||||||
|
end
|
||||||
|
|
||||||
|
private def initialize_websocket : Discord::WebSocket
|
||||||
|
url = URI.parse(get_gateway.url)
|
||||||
|
websocket = Discord::WebSocket.new(
|
||||||
|
host: url.host.not_nil!,
|
||||||
|
path: "#{url.path}/?encoding=json&v=6",
|
||||||
|
port: 443,
|
||||||
|
tls: true
|
||||||
|
)
|
||||||
|
|
||||||
|
websocket.on_message(&->on_message(Discord::WebSocket::Packet))
|
||||||
|
websocket.on_close(&->on_close(String))
|
||||||
|
|
||||||
|
websocket
|
||||||
|
end
|
||||||
|
|
||||||
|
private def on_close(message : String)
|
||||||
|
# TODO: make more sophisticated
|
||||||
|
LOGGER.warn "Closed with: " + message
|
||||||
|
|
||||||
|
@send_heartbeats = false
|
||||||
|
@session.try &.suspend
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
OP_DISPATCH = 0
|
||||||
|
OP_HEARTBEAT = 1
|
||||||
|
OP_IDENTIFY = 2
|
||||||
|
OP_STATUS_UPDATE = 3
|
||||||
|
OP_VOICE_STATE_UPDATE = 4
|
||||||
|
OP_VOICE_SERVER_PING = 5
|
||||||
|
OP_RESUME = 6
|
||||||
|
OP_RECONNECT = 7
|
||||||
|
OP_REQUEST_GUILD_MEMBERS = 8
|
||||||
|
OP_INVALID_SESSION = 9
|
||||||
|
OP_HELLO = 10
|
||||||
|
OP_HEARTBEAT_ACK = 11
|
||||||
|
|
||||||
|
private def on_message(packet : Discord::WebSocket::Packet)
|
||||||
|
spawn do
|
||||||
|
begin
|
||||||
|
case packet.opcode
|
||||||
|
when OP_HELLO
|
||||||
|
payload = Gateway::HelloPayload.from_json(packet.data)
|
||||||
|
handle_hello(payload.heartbeat_interval)
|
||||||
|
when OP_DISPATCH
|
||||||
|
handle_dispatch(packet.event_type.not_nil!, packet.data)
|
||||||
|
when OP_RECONNECT
|
||||||
|
handle_reconnect
|
||||||
|
when OP_INVALID_SESSION
|
||||||
|
handle_invalid_session
|
||||||
|
when OP_HEARTBEAT
|
||||||
|
# We got a received heartbeat, reply with the same sequence
|
||||||
|
LOGGER.debug "Heartbeat received"
|
||||||
|
websocket.send({op: 1, d: packet.sequence}.to_json)
|
||||||
|
when OP_HEARTBEAT_ACK
|
||||||
|
handle_heartbeat_ack
|
||||||
|
else
|
||||||
|
LOGGER.warn "Unsupported payload: #{packet}"
|
||||||
|
end
|
||||||
|
rescue ex : JSON::ParseException
|
||||||
|
LOGGER.error <<-LOG
|
||||||
|
An exception occurred during message parsing! Please report this.
|
||||||
|
#{ex.inspect_with_backtrace}
|
||||||
|
(pertaining to previous exception) Raised with packet:
|
||||||
|
#{packet}
|
||||||
|
LOG
|
||||||
|
rescue ex
|
||||||
|
LOGGER.error <<-LOG
|
||||||
|
A miscellaneous exception occurred during message handling.
|
||||||
|
#{ex.inspect_with_backtrace}
|
||||||
|
LOG
|
||||||
|
end
|
||||||
|
|
||||||
|
# Set the sequence to confirm that we have handled this packet, in case
|
||||||
|
# we need to resume
|
||||||
|
seq = packet.sequence
|
||||||
|
@session.try &.sequence = seq if seq
|
||||||
|
end
|
||||||
|
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
# Injects a *packet* into the packet handler.
|
||||||
|
def inject(packet : Discord::WebSocket::Packet)
|
||||||
|
on_message(packet)
|
||||||
|
end
|
||||||
|
|
||||||
|
private def handle_hello(heartbeat_interval)
|
||||||
|
@heartbeat_interval = heartbeat_interval
|
||||||
|
@send_heartbeats = true
|
||||||
|
@last_heartbeat_acked = true
|
||||||
|
|
||||||
|
# If it seems like we can resume, we will - worst case we get an op9
|
||||||
|
if @session.try &.should_resume?
|
||||||
|
resume
|
||||||
|
else
|
||||||
|
identify
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private def setup_heartbeats
|
||||||
|
spawn do
|
||||||
|
loop do
|
||||||
|
if @send_heartbeats
|
||||||
|
unless @last_heartbeat_acked
|
||||||
|
LOGGER.warn "Last heartbeat not acked, reconnecting"
|
||||||
|
|
||||||
|
# Give the new connection another chance by resetting the last
|
||||||
|
# acked flag; otherwise it would try to reconnect again at the
|
||||||
|
# first heartbeat
|
||||||
|
@last_heartbeat_acked = true
|
||||||
|
|
||||||
|
reconnect(should_suspend: true)
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
LOGGER.debug "Sending heartbeat"
|
||||||
|
|
||||||
|
begin
|
||||||
|
seq = @session.try &.sequence || 0
|
||||||
|
websocket.send({op: 1, d: seq}.to_json)
|
||||||
|
@last_heartbeat_acked = false
|
||||||
|
rescue ex
|
||||||
|
LOGGER.error <<-LOG
|
||||||
|
Heartbeat failed!
|
||||||
|
#{ex.inspect_with_backtrace}
|
||||||
|
LOG
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
sleep @heartbeat_interval.milliseconds
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private def identify
|
||||||
|
if shard = @shard
|
||||||
|
shard_tuple = shard.values
|
||||||
|
end
|
||||||
|
|
||||||
|
packet = Gateway::IdentifyPacket.new(@token, @properties, @compress, @large_threshold, shard_tuple)
|
||||||
|
websocket.send(packet.to_json)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Sends a resume packet from the given *sequence* number, or alternatively
|
||||||
|
# the current session's last received sequence if none is given. This will
|
||||||
|
# make Discord replay all events since that sequence.
|
||||||
|
def resume(sequence : Int64? = nil)
|
||||||
|
session = @session.not_nil!
|
||||||
|
sequence ||= session.sequence
|
||||||
|
|
||||||
|
packet = Gateway::ResumePacket.new(@token, session.session_id, sequence)
|
||||||
|
websocket.send(packet.to_json)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Reconnects the websocket connection entirely. If *should_suspend* is set,
|
||||||
|
# the session will be suspended, which means (unless other factors prevent
|
||||||
|
# this) that the session will be resumed after reconnection. If
|
||||||
|
# *backoff_override* is set to anything other than `nil`, the reconnection
|
||||||
|
# backoff will not use the standard formula and instead wait the value
|
||||||
|
# provided; use `0.0` to skip waiting entirely.
|
||||||
|
def reconnect(should_suspend = false, backoff_override = nil)
|
||||||
|
@backoff = backoff_override if backoff_override
|
||||||
|
@send_heartbeats = false
|
||||||
|
websocket.close
|
||||||
|
|
||||||
|
# Suspend the session so we resume, if desired
|
||||||
|
@session.try &.suspend if should_suspend
|
||||||
|
end
|
||||||
|
|
||||||
|
# Sends a status update to Discord. The *status* can be `"online"`,
|
||||||
|
# `"idle"`, `"dnd"`, or `"invisible"`. Setting the *game* to a `GamePlaying`
|
||||||
|
# object makes the bot appear as playing some game on Discord. *since* and
|
||||||
|
# *afk* can be used in conjunction to signify to Discord that the status
|
||||||
|
# change is due to inactivity on the bot's part – this fulfills no cosmetic
|
||||||
|
# purpose.
|
||||||
|
def status_update(status : String? = nil, game : GamePlaying? = nil, afk : Bool = false, since : Int64? = nil)
|
||||||
|
packet = Gateway::StatusUpdatePacket.new(status, game, afk, since)
|
||||||
|
websocket.send(packet.to_json)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Sends a voice state update to Discord. This will create a new voice
|
||||||
|
# connection on the given *guild_id* and *channel_id*, update an existing
|
||||||
|
# one with new *self_mute* and *self_deaf* status, or disconnect from voice
|
||||||
|
# if the *channel_id* is `nil`.
|
||||||
|
#
|
||||||
|
# discordcr doesn't support sending or receiving any data from voice
|
||||||
|
# connections yet - this will have to be done externally until that happens.
|
||||||
|
def voice_state_update(guild_id : UInt64, channel_id : UInt64?, self_mute : Bool, self_deaf : Bool)
|
||||||
|
packet = Gateway::VoiceStateUpdatePacket.new(guild_id, channel_id, self_mute, self_deaf)
|
||||||
|
websocket.send(packet.to_json)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Requests a full list of members to be sent for a specific guild. This is
|
||||||
|
# necessary to get the entire members list for guilds considered large (what
|
||||||
|
# is considered large can be changed using the large_threshold parameter
|
||||||
|
# in `#initialize`).
|
||||||
|
#
|
||||||
|
# The list will arrive in the form of GUILD_MEMBERS_CHUNK dispatch events,
|
||||||
|
# which can be listened to using `#on_guild_members_chunk`. If a cache
|
||||||
|
# is set up, arriving members will be cached automatically.
|
||||||
|
def request_guild_members(guild_id : UInt64, query : String = "", limit : Int32 = 0)
|
||||||
|
packet = Gateway::RequestGuildMembersPacket.new(guild_id, query, limit)
|
||||||
|
websocket.send(packet.to_json)
|
||||||
|
end
|
||||||
|
|
||||||
|
# :nodoc:
|
||||||
|
macro call_event(name, payload)
|
||||||
|
@on_{{name}}_handlers.try &.each do |handler|
|
||||||
|
begin
|
||||||
|
handler.call({{payload}})
|
||||||
|
rescue ex
|
||||||
|
LOGGER.error <<-LOG
|
||||||
|
An exception occurred in a user-defined event handler!
|
||||||
|
#{ex.inspect_with_backtrace}
|
||||||
|
LOG
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# :nodoc:
|
||||||
|
macro cache(object)
|
||||||
|
@cache.try &.cache {{object}}
|
||||||
|
end
|
||||||
|
|
||||||
|
private def handle_dispatch(type, data)
|
||||||
|
call_event dispatch, {type, data}
|
||||||
|
|
||||||
|
case type
|
||||||
|
when "READY"
|
||||||
|
payload = Gateway::ReadyPayload.from_json(data)
|
||||||
|
|
||||||
|
@session = Gateway::Session.new(payload.session_id)
|
||||||
|
|
||||||
|
# Reset the backoff, because READY means we successfully achieved a
|
||||||
|
# connection and don't have to wait next time
|
||||||
|
@backoff = 1.0
|
||||||
|
|
||||||
|
@cache.try &.cache_current_user(payload.user)
|
||||||
|
|
||||||
|
payload.private_channels.each do |channel|
|
||||||
|
cache Channel.new(channel)
|
||||||
|
|
||||||
|
if channel.type == 1 # DM channel, not group
|
||||||
|
recipient_id = channel.recipients[0].id
|
||||||
|
@cache.try &.cache_dm_channel(channel.id, recipient_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
LOGGER.info "Received READY, v: #{payload.v}"
|
||||||
|
call_event ready, payload
|
||||||
|
when "RESUMED"
|
||||||
|
# RESUMED also means a connection was achieved, so reset the
|
||||||
|
# reconnection backoff here too
|
||||||
|
@backoff = 1.0
|
||||||
|
|
||||||
|
payload = Gateway::ResumedPayload.from_json(data)
|
||||||
|
call_event resumed, payload
|
||||||
|
when "CHANNEL_CREATE"
|
||||||
|
payload = Channel.from_json(data)
|
||||||
|
|
||||||
|
cache payload
|
||||||
|
guild_id = payload.guild_id
|
||||||
|
recipients = payload.recipients
|
||||||
|
if guild_id
|
||||||
|
@cache.try &.add_guild_channel(guild_id, payload.id)
|
||||||
|
elsif payload.type == 1 && recipients
|
||||||
|
@cache.try &.cache_dm_channel(payload.id, recipients[0].id)
|
||||||
|
end
|
||||||
|
|
||||||
|
call_event channel_create, payload
|
||||||
|
when "CHANNEL_UPDATE"
|
||||||
|
payload = Channel.from_json(data)
|
||||||
|
|
||||||
|
cache payload
|
||||||
|
|
||||||
|
call_event channel_update, payload
|
||||||
|
when "CHANNEL_DELETE"
|
||||||
|
payload = Channel.from_json(data)
|
||||||
|
|
||||||
|
@cache.try &.delete_channel(payload.id)
|
||||||
|
guild_id = payload.guild_id
|
||||||
|
@cache.try &.remove_guild_channel(guild_id, payload.id) if guild_id
|
||||||
|
|
||||||
|
call_event channel_delete, payload
|
||||||
|
when "CHANNEL_PINS_UPDATE"
|
||||||
|
payload = Gateway::ChannelPinsUpdatePayload.from_json(data)
|
||||||
|
call_event channel_pins_update, payload
|
||||||
|
when "GUILD_CREATE"
|
||||||
|
payload = Gateway::GuildCreatePayload.from_json(data)
|
||||||
|
|
||||||
|
guild = Guild.new(payload)
|
||||||
|
cache guild
|
||||||
|
|
||||||
|
payload.channels.each do |channel|
|
||||||
|
channel.guild_id = guild.id
|
||||||
|
cache channel
|
||||||
|
@cache.try &.add_guild_channel(guild.id, channel.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
payload.roles.each do |role|
|
||||||
|
cache role
|
||||||
|
@cache.try &.add_guild_role(guild.id, role.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
payload.members.each do |member|
|
||||||
|
cache member.user
|
||||||
|
@cache.try &.cache(member, guild.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
call_event guild_create, payload
|
||||||
|
when "GUILD_UPDATE"
|
||||||
|
payload = Guild.from_json(data)
|
||||||
|
|
||||||
|
cache payload
|
||||||
|
|
||||||
|
call_event guild_update, payload
|
||||||
|
when "GUILD_DELETE"
|
||||||
|
payload = Gateway::GuildDeletePayload.from_json(data)
|
||||||
|
|
||||||
|
@cache.try &.delete_guild(payload.id)
|
||||||
|
|
||||||
|
call_event guild_delete, payload
|
||||||
|
when "GUILD_BAN_ADD"
|
||||||
|
payload = Gateway::GuildBanPayload.from_json(data)
|
||||||
|
call_event guild_ban_add, payload
|
||||||
|
when "GUILD_BAN_REMOVE"
|
||||||
|
payload = Gateway::GuildBanPayload.from_json(data)
|
||||||
|
call_event guild_ban_remove, payload
|
||||||
|
when "GUILD_EMOJIS_UPDATE"
|
||||||
|
payload = Gateway::GuildEmojiUpdatePayload.from_json(data)
|
||||||
|
call_event guild_emoji_update, payload
|
||||||
|
when "GUILD_INTEGRATIONS_UPDATE"
|
||||||
|
payload = Gateway::GuildIntegrationsUpdatePayload.from_json(data)
|
||||||
|
call_event guild_integrations_update, payload
|
||||||
|
when "GUILD_MEMBER_ADD"
|
||||||
|
payload = Gateway::GuildMemberAddPayload.from_json(data)
|
||||||
|
|
||||||
|
cache payload.user
|
||||||
|
member = GuildMember.new(payload)
|
||||||
|
@cache.try &.cache(member, payload.guild_id)
|
||||||
|
|
||||||
|
call_event guild_member_add, payload
|
||||||
|
when "GUILD_MEMBER_UPDATE"
|
||||||
|
payload = Gateway::GuildMemberUpdatePayload.from_json(data)
|
||||||
|
|
||||||
|
cache payload.user
|
||||||
|
@cache.try do |c|
|
||||||
|
member = c.resolve_member(payload.guild_id, payload.user.id)
|
||||||
|
new_member = GuildMember.new(member, payload.roles, payload.nick)
|
||||||
|
c.cache(new_member, payload.guild_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
call_event guild_member_update, payload
|
||||||
|
when "GUILD_MEMBER_REMOVE"
|
||||||
|
payload = Gateway::GuildMemberRemovePayload.from_json(data)
|
||||||
|
|
||||||
|
cache payload.user
|
||||||
|
@cache.try &.delete_member(payload.guild_id, payload.user.id)
|
||||||
|
|
||||||
|
call_event guild_member_remove, payload
|
||||||
|
when "GUILD_MEMBERS_CHUNK"
|
||||||
|
payload = Gateway::GuildMembersChunkPayload.from_json(data)
|
||||||
|
|
||||||
|
@cache.try &.cache_multiple_members(payload.members, payload.guild_id)
|
||||||
|
|
||||||
|
call_event guild_members_chunk, payload
|
||||||
|
when "GUILD_ROLE_CREATE"
|
||||||
|
payload = Gateway::GuildRolePayload.from_json(data)
|
||||||
|
|
||||||
|
cache payload.role
|
||||||
|
@cache.try &.add_guild_role(payload.guild_id, payload.role.id)
|
||||||
|
|
||||||
|
call_event guild_role_create, payload
|
||||||
|
when "GUILD_ROLE_UPDATE"
|
||||||
|
payload = Gateway::GuildRolePayload.from_json(data)
|
||||||
|
|
||||||
|
cache payload.role
|
||||||
|
|
||||||
|
call_event guild_role_update, payload
|
||||||
|
when "GUILD_ROLE_DELETE"
|
||||||
|
payload = Gateway::GuildRoleDeletePayload.from_json(data)
|
||||||
|
|
||||||
|
@cache.try &.delete_role(payload.role_id)
|
||||||
|
@cache.try &.remove_guild_role(payload.guild_id, payload.role_id)
|
||||||
|
|
||||||
|
call_event guild_role_delete, payload
|
||||||
|
when "MESSAGE_CREATE"
|
||||||
|
payload = Message.from_json(data)
|
||||||
|
LOGGER.debug "Received message with content #{payload.content}"
|
||||||
|
call_event message_create, payload
|
||||||
|
when "MESSAGE_REACTION_ADD"
|
||||||
|
payload = Gateway::MessageReactionPayload.from_json(data)
|
||||||
|
call_event message_reaction_add, payload
|
||||||
|
when "MESSAGE_REACTION_REMOVE"
|
||||||
|
payload = Gateway::MessageReactionPayload.from_json(data)
|
||||||
|
call_event message_reaction_remove, payload
|
||||||
|
when "MESSAGE_REACTION_REMOVE_ALL"
|
||||||
|
payload = Gateway::MessageReactionRemoveAllPayload.from_json(data)
|
||||||
|
call_event message_reaction_remove_all, payload
|
||||||
|
when "MESSAGE_UPDATE"
|
||||||
|
payload = Gateway::MessageUpdatePayload.from_json(data)
|
||||||
|
call_event message_update, payload
|
||||||
|
when "MESSAGE_DELETE"
|
||||||
|
payload = Gateway::MessageDeletePayload.from_json(data)
|
||||||
|
call_event message_delete, payload
|
||||||
|
when "MESSAGE_DELETE_BULK"
|
||||||
|
payload = Gateway::MessageDeleteBulkPayload.from_json(data)
|
||||||
|
call_event message_delete_bulk, payload
|
||||||
|
when "PRESENCE_UPDATE"
|
||||||
|
payload = Gateway::PresenceUpdatePayload.from_json(data)
|
||||||
|
|
||||||
|
if payload.user.full?
|
||||||
|
member = GuildMember.new(payload)
|
||||||
|
@cache.try &.cache(member, payload.guild_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
call_event presence_update, payload
|
||||||
|
when "TYPING_START"
|
||||||
|
payload = Gateway::TypingStartPayload.from_json(data)
|
||||||
|
call_event typing_start, payload
|
||||||
|
when "USER_UPDATE"
|
||||||
|
payload = User.from_json(data)
|
||||||
|
call_event user_update, payload
|
||||||
|
when "VOICE_STATE_UPDATE"
|
||||||
|
payload = VoiceState.from_json(data)
|
||||||
|
call_event voice_state_update, payload
|
||||||
|
when "VOICE_SERVER_UPDATE"
|
||||||
|
payload = Gateway::VoiceServerUpdatePayload.from_json(data)
|
||||||
|
call_event voice_server_update, payload
|
||||||
|
when "WEBHOOKS_UPDATE"
|
||||||
|
payload = Gateway::WebhooksUpdatePayload.from_json(data)
|
||||||
|
call_event webhooks_update, payload
|
||||||
|
else
|
||||||
|
LOGGER.warn "Unsupported dispatch: #{type} #{data}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private def handle_reconnect
|
||||||
|
# We want the reconnection to happen instantly, and we want a resume to be
|
||||||
|
# attempted, so set the respective parameters
|
||||||
|
reconnect(should_suspend: true, backoff_override: 0.0)
|
||||||
|
end
|
||||||
|
|
||||||
|
private def handle_invalid_session
|
||||||
|
@session.try &.invalidate
|
||||||
|
identify
|
||||||
|
end
|
||||||
|
|
||||||
|
private def handle_heartbeat_ack
|
||||||
|
LOGGER.debug "Heartbeat ACK received"
|
||||||
|
@last_heartbeat_acked = true
|
||||||
|
end
|
||||||
|
|
||||||
|
# :nodoc:
|
||||||
|
macro event(name, payload_type)
|
||||||
|
def on_{{name}}(&handler : {{payload_type}} ->)
|
||||||
|
(@on_{{name}}_handlers ||= [] of {{payload_type}} ->) << handler
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Called when the bot receives any kind of dispatch at all, even one that
|
||||||
|
# is otherwise unsupported. This can be useful for statistics, e. g. how
|
||||||
|
# many gateway events are received per second. It can also be useful to
|
||||||
|
# handle new API changes not yet supported by the lib.
|
||||||
|
#
|
||||||
|
# The parameter passed to the event will be a tuple of `{type, data}`, where
|
||||||
|
# `type` is the event type (e.g. "MESSAGE_CREATE") and `data` is the
|
||||||
|
# unprocessed JSON event data.
|
||||||
|
event dispatch, {String, IO::Memory}
|
||||||
|
|
||||||
|
# Called when the bot has successfully initiated a session with Discord. It
|
||||||
|
# marks the point when gateway packets can be set (e. g. `#status_update`).
|
||||||
|
#
|
||||||
|
# Note that this event may be called multiple times over the course of a
|
||||||
|
# bot lifetime, as it is also called when the client reconnects with a new
|
||||||
|
# session.
|
||||||
|
#
|
||||||
|
# [API docs for this event](https://discordapp.com/developers/docs/topics/gateway#ready)
|
||||||
|
event ready, Gateway::ReadyPayload
|
||||||
|
|
||||||
|
# Called when the client has successfully resumed an existing connection
|
||||||
|
# after reconnecting.
|
||||||
|
#
|
||||||
|
# [API docs for this event](https://discordapp.com/developers/docs/topics/gateway#resumed)
|
||||||
|
event resumed, Gateway::ResumedPayload
|
||||||
|
|
||||||
|
# Called when a channel has been created on a server the bot has access to,
|
||||||
|
# or when somebody has started a DM channel with the bot.
|
||||||
|
#
|
||||||
|
# [API docs for this event](https://discordapp.com/developers/docs/topics/gateway#channel-create)
|
||||||
|
event channel_create, Channel
|
||||||
|
|
||||||
|
# Called when a channel's properties are updated, like the name or
|
||||||
|
# permission overwrites.
|
||||||
|
#
|
||||||
|
# [API docs for this event](https://discordapp.com/developers/docs/topics/gateway#channel-update)
|
||||||
|
event channel_update, Channel
|
||||||
|
|
||||||
|
# Called when a channel the bot has access to is deleted. This is not called
|
||||||
|
# for other users closing the DM channel with the bot, only for the bot
|
||||||
|
# closing the DM channel with a user.
|
||||||
|
#
|
||||||
|
# [API docs for this event](https://discordapp.com/developers/docs/topics/gateway#channel-delete)
|
||||||
|
event channel_delete, Channel
|
||||||
|
|
||||||
|
# Called when a channel's pinned messages are updated, where a pin was
|
||||||
|
# either added or removed.
|
||||||
|
#
|
||||||
|
# [API docs for this event](https://discordapp.com/developers/docs/topics/gateway#channel-pins-update)
|
||||||
|
event channel_pins_update, Gateway::ChannelPinsUpdatePayload
|
||||||
|
|
||||||
|
# Called when the bot is added to a guild, a guild unavailable due to an
|
||||||
|
# outage becomes available again, or the guild is streamed after READY.
|
||||||
|
# To verify that it is the first case, you can check the `unavailable`
|
||||||
|
# property in `Gateway::GuildCreatePayload`.
|
||||||
|
#
|
||||||
|
# [API docs for this event](https://discordapp.com/developers/docs/topics/gateway#guild-create)
|
||||||
|
event guild_create, Gateway::GuildCreatePayload
|
||||||
|
|
||||||
|
# Called when a guild's properties, like name or verification level, are
|
||||||
|
# updated.
|
||||||
|
#
|
||||||
|
# [API docs for this event](https://discordapp.com/developers/docs/topics/gateway#guild-update)
|
||||||
|
event guild_update, Guild
|
||||||
|
|
||||||
|
# Called when the bot leaves a guild or a guild becomes unavailable due to
|
||||||
|
# an outage. To verify that it is the former case, you can check the
|
||||||
|
# `unavailable` property.
|
||||||
|
#
|
||||||
|
# [API docs for this event](https://discordapp.com/developers/docs/topics/gateway#guild-delete)
|
||||||
|
event guild_delete, Gateway::GuildDeletePayload
|
||||||
|
|
||||||
|
# Called when somebody is banned from a guild. A `#on_guild_member_remove`
|
||||||
|
# event is also called.
|
||||||
|
#
|
||||||
|
# [API docs for this event](https://discordapp.com/developers/docs/topics/gateway#guild-ban-add)
|
||||||
|
event guild_ban_add, Gateway::GuildBanPayload
|
||||||
|
|
||||||
|
# Called when somebody is unbanned from a guild.
|
||||||
|
#
|
||||||
|
# [API docs for this event](https://discordapp.com/developers/docs/topics/gateway#guild-ban-remove)
|
||||||
|
event guild_ban_remove, Gateway::GuildBanPayload
|
||||||
|
|
||||||
|
# Called when a guild's emoji are updated.
|
||||||
|
#
|
||||||
|
# [API docs for this event](https://discordapp.com/developers/docs/topics/gateway#guild-emoji-update)
|
||||||
|
event guild_emoji_update, Gateway::GuildEmojiUpdatePayload
|
||||||
|
|
||||||
|
# Called when a guild's integrations (Twitch, YouTube) are updated.
|
||||||
|
#
|
||||||
|
# [API docs for this event](https://discordapp.com/developers/docs/topics/gateway#guild-integrations-update)
|
||||||
|
event guild_integrations_update, Gateway::GuildIntegrationsUpdatePayload
|
||||||
|
|
||||||
|
# Called when somebody other than the bot joins a guild.
|
||||||
|
#
|
||||||
|
# [API docs for this event](https://discordapp.com/developers/docs/topics/gateway#guild-member-add)
|
||||||
|
event guild_member_add, Gateway::GuildMemberAddPayload
|
||||||
|
|
||||||
|
# Called when a member object is updated. This happens when somebody
|
||||||
|
# changes their nickname or has their roles changed.
|
||||||
|
#
|
||||||
|
# [API docs for this event](https://discordapp.com/developers/docs/topics/gateway#guild-member-update)
|
||||||
|
event guild_member_update, Gateway::GuildMemberUpdatePayload
|
||||||
|
|
||||||
|
# Called when somebody other than the bot leaves a guild.
|
||||||
|
#
|
||||||
|
# [API docs for this event](https://discordapp.com/developers/docs/topics/gateway#guild-member-remove)
|
||||||
|
event guild_member_remove, Gateway::GuildMemberRemovePayload
|
||||||
|
|
||||||
|
# Called when Discord sends a chunk of member objects after a
|
||||||
|
# `#request_guild_members` call. If a `Cache` is set up, this is handled
|
||||||
|
# automatically.
|
||||||
|
#
|
||||||
|
# [API docs for this event](https://discordapp.com/developers/docs/topics/gateway#guild-members-chunk)
|
||||||
|
event guild_members_chunk, Gateway::GuildMembersChunkPayload
|
||||||
|
|
||||||
|
# Called when a role is created on a guild.
|
||||||
|
#
|
||||||
|
# [API docs for this event](https://discordapp.com/developers/docs/topics/gateway#guild-role-create)
|
||||||
|
event guild_role_create, Gateway::GuildRolePayload
|
||||||
|
|
||||||
|
# Called when a role's properties are updated, for example name or colour.
|
||||||
|
#
|
||||||
|
# [API docs for this event](https://discordapp.com/developers/docs/topics/gateway#guild-role-update)
|
||||||
|
event guild_role_update, Gateway::GuildRolePayload
|
||||||
|
|
||||||
|
# Called when a role is deleted.
|
||||||
|
#
|
||||||
|
# [API docs for this event](https://discordapp.com/developers/docs/topics/gateway#guild-role-delete)
|
||||||
|
event guild_role_delete, Gateway::GuildRoleDeletePayload
|
||||||
|
|
||||||
|
# Called when a message is sent to a channel the bot has access to. This
|
||||||
|
# may be any sort of text channel, no matter private or guild.
|
||||||
|
#
|
||||||
|
# [API docs for this event](https://discordapp.com/developers/docs/topics/gateway#message-create)
|
||||||
|
event message_create, Message
|
||||||
|
|
||||||
|
# Called when a reaction is added to a message.
|
||||||
|
event message_reaction_add, Gateway::MessageReactionPayload
|
||||||
|
|
||||||
|
# Called when a reaction is removed from a message.
|
||||||
|
event message_reaction_remove, Gateway::MessageReactionPayload
|
||||||
|
|
||||||
|
# Called when all reactions are removed at once from a message.
|
||||||
|
event message_reaction_remove_all, Gateway::MessageReactionRemoveAllPayload
|
||||||
|
|
||||||
|
# Called when a message is updated. Most commonly this is done for edited
|
||||||
|
# messages, but the event is also sent when embed information for an
|
||||||
|
# existing message is updated.
|
||||||
|
#
|
||||||
|
# [API docs for this event](https://discordapp.com/developers/docs/topics/gateway#message-update)
|
||||||
|
event message_update, Gateway::MessageUpdatePayload
|
||||||
|
|
||||||
|
# Called when a single message is deleted.
|
||||||
|
#
|
||||||
|
# [API docs for this event](https://discordapp.com/developers/docs/topics/gateway#message-delete)
|
||||||
|
event message_delete, Gateway::MessageDeletePayload
|
||||||
|
|
||||||
|
# Called when multiple messages are deleted at once, due to a bot using the
|
||||||
|
# bulk_delete endpoint.
|
||||||
|
#
|
||||||
|
# [API docs for this event](https://discordapp.com/developers/docs/topics/gateway#message-delete-bulk)
|
||||||
|
event message_delete_bulk, Gateway::MessageDeleteBulkPayload
|
||||||
|
|
||||||
|
# Called when a user updates their status (online/idle/offline), the game
|
||||||
|
# they are playing, or their streaming status. Also called when a user's
|
||||||
|
# properties (user/avatar/discriminator) are changed.
|
||||||
|
#
|
||||||
|
# [API docs for this event](https://discordapp.com/developers/docs/topics/gateway#presence-update)
|
||||||
|
event presence_update, Gateway::PresenceUpdatePayload
|
||||||
|
|
||||||
|
# Called when somebody starts typing in a channel the bot has access to.
|
||||||
|
#
|
||||||
|
# [API docs for this event](https://discordapp.com/developers/docs/topics/gateway#typing-start)
|
||||||
|
event typing_start, Gateway::TypingStartPayload
|
||||||
|
|
||||||
|
# Called when the user properties of the bot itself are changed.
|
||||||
|
#
|
||||||
|
# [API docs for this event](https://discordapp.com/developers/docs/topics/gateway#user-update)
|
||||||
|
event user_update, User
|
||||||
|
|
||||||
|
# Called when somebody joins or leaves a voice channel, moves to a different
|
||||||
|
# one, or is muted/unmuted/deafened/undeafened.
|
||||||
|
#
|
||||||
|
# [API docs for this event](https://discordapp.com/developers/docs/topics/gateway#voice-state-update)
|
||||||
|
event voice_state_update, VoiceState
|
||||||
|
|
||||||
|
# Called when a guild's voice server changes. This event is called with
|
||||||
|
# the current voice server when initially connecting to voice, and it is
|
||||||
|
# called again with the new voice server when the current server fails over
|
||||||
|
# to a new one, or when the guild's voice region changes.
|
||||||
|
#
|
||||||
|
# [API docs for this event](https://discordapp.com/developers/docs/topics/gateway#voice-server-update)
|
||||||
|
event voice_server_update, Gateway::VoiceServerUpdatePayload
|
||||||
|
|
||||||
|
# Sent when a guild channel's webhook is created, updated, or deleted.
|
||||||
|
#
|
||||||
|
# [API docs for this event](https://discordapp.com/developers/docs/topics/gateway#webhooks-update)
|
||||||
|
event webhooks_update, Gateway::WebhooksUpdatePayload
|
||||||
|
end
|
||||||
|
|
||||||
|
module Gateway
|
||||||
|
alias ShardKey = {shard_id: Int32, num_shards: Int32}
|
||||||
|
|
||||||
|
class Session
|
||||||
|
getter session_id
|
||||||
|
property sequence
|
||||||
|
|
||||||
|
def initialize(@session_id : String)
|
||||||
|
@sequence = 0_i64
|
||||||
|
|
||||||
|
@suspended = false
|
||||||
|
@invalid = false
|
||||||
|
end
|
||||||
|
|
||||||
|
def suspend
|
||||||
|
@suspended = true
|
||||||
|
end
|
||||||
|
|
||||||
|
def suspended? : Bool
|
||||||
|
@suspended
|
||||||
|
end
|
||||||
|
|
||||||
|
def invalidate
|
||||||
|
@invalid = true
|
||||||
|
end
|
||||||
|
|
||||||
|
def invalid? : Bool
|
||||||
|
@invalid
|
||||||
|
end
|
||||||
|
|
||||||
|
def should_resume? : Bool
|
||||||
|
suspended? && !invalid?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
150
Crystal/First-Program/lib/discordcr/src/discordcr/dca.cr
Normal file
150
Crystal/First-Program/lib/discordcr/src/discordcr/dca.cr
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
require "json"
|
||||||
|
|
||||||
|
module Discord
|
||||||
|
# Parser for the DCA file format, a simple wrapper around Opus made
|
||||||
|
# specifically for Discord bots.
|
||||||
|
class DCAParser
|
||||||
|
# Magic string that identifies a DCA1 file
|
||||||
|
DCA1_MAGIC = "DCA1"
|
||||||
|
|
||||||
|
# The parsed metadata, or nil if it could not be parsed.
|
||||||
|
getter metadata : DCA1Mappings::Metadata?
|
||||||
|
|
||||||
|
# Create a new parser. It will read from the given *io*. If *raw* is set,
|
||||||
|
# the file is assumed to be a DCA0 file, without any metadata. If the file's
|
||||||
|
# metadata doesn't conform to the DCA1 specification and *strict_metadata*
|
||||||
|
# is set, then the parsing will fail with an error; if it is not set then
|
||||||
|
# the metadata will silently be `nil`.
|
||||||
|
def initialize(@io : IO, raw = false, @strict_metadata = true)
|
||||||
|
unless raw
|
||||||
|
verify_magic
|
||||||
|
parse_metadata
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Reads the next frame from the IO. If there is nothing left to read, it
|
||||||
|
# will return `nil`.
|
||||||
|
#
|
||||||
|
# If *reuse_buffer* is true, a large buffer will be allocated once and
|
||||||
|
# reused for future calls of this method, reducing the load on the GC and
|
||||||
|
# potentially reusing memory use overall; if it is false, a new buffer of
|
||||||
|
# just the correct size will be allocated every time. Note that if the
|
||||||
|
# buffer is reused, the returned data is only valid until the next call to
|
||||||
|
# `next_frame`.
|
||||||
|
def next_frame(reuse_buffer = false) : Bytes?
|
||||||
|
begin
|
||||||
|
header = @io.read_bytes(Int16, IO::ByteFormat::LittleEndian)
|
||||||
|
raise "Negative frame header (#{header} < 0)" if header < 0
|
||||||
|
|
||||||
|
buf = if reuse_buffer
|
||||||
|
full_buf = @reused_buffer ||= Bytes.new(Int16::MAX)
|
||||||
|
full_buf[0, header]
|
||||||
|
else
|
||||||
|
Bytes.new(header)
|
||||||
|
end
|
||||||
|
|
||||||
|
@io.read_fully(buf)
|
||||||
|
buf
|
||||||
|
rescue IO::EOFError
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Continually reads frames from the IO until there are none left. Each frame
|
||||||
|
# is passed to the given *block*.
|
||||||
|
def parse(&block : Bytes ->)
|
||||||
|
loop do
|
||||||
|
buf = next_frame
|
||||||
|
|
||||||
|
if buf
|
||||||
|
block.call(buf)
|
||||||
|
else
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private def verify_magic
|
||||||
|
magic = @io.read_string(4)
|
||||||
|
if magic != DCA1_MAGIC
|
||||||
|
raise "File is not a DCA1 file (magic is #{magic}, should be DCA1)"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private def parse_metadata
|
||||||
|
# The header of the metadata part is the four-byte size of the following
|
||||||
|
# metadata payload.
|
||||||
|
metadata_size = @io.read_bytes(Int32, IO::ByteFormat::LittleEndian)
|
||||||
|
metadata_io = IO::Sized.new(@io, read_size: metadata_size)
|
||||||
|
|
||||||
|
begin
|
||||||
|
@metadata = DCA1Mappings::Metadata.from_json(metadata_io)
|
||||||
|
rescue e : JSON::ParseException
|
||||||
|
raise e if @strict_metadata
|
||||||
|
end
|
||||||
|
|
||||||
|
metadata_io.skip_to_end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Mappings for DCA1 metadata
|
||||||
|
module DCA1Mappings
|
||||||
|
struct Metadata
|
||||||
|
JSON.mapping(
|
||||||
|
dca: DCA,
|
||||||
|
opus: Opus,
|
||||||
|
info: Info?,
|
||||||
|
origin: Origin?,
|
||||||
|
extra: JSON::Any
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct DCA
|
||||||
|
JSON.mapping(
|
||||||
|
version: Int32,
|
||||||
|
tool: Tool
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct Tool
|
||||||
|
JSON.mapping(
|
||||||
|
name: String,
|
||||||
|
version: String,
|
||||||
|
url: String?,
|
||||||
|
author: String?
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct Opus
|
||||||
|
JSON.mapping(
|
||||||
|
mode: String,
|
||||||
|
sample_rate: Int32,
|
||||||
|
frame_size: Int32,
|
||||||
|
abr: Int32?,
|
||||||
|
vbr: Bool,
|
||||||
|
channels: Int32
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct Info
|
||||||
|
JSON.mapping(
|
||||||
|
title: String?,
|
||||||
|
artist: String?,
|
||||||
|
album: String?,
|
||||||
|
genre: String?,
|
||||||
|
comments: String?,
|
||||||
|
cover: String?
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct Origin
|
||||||
|
JSON.mapping(
|
||||||
|
source: String?,
|
||||||
|
abr: Int32?,
|
||||||
|
channels: Int32?,
|
||||||
|
encoding: String?,
|
||||||
|
url: String?
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
71
Crystal/First-Program/lib/discordcr/src/discordcr/errors.cr
Normal file
71
Crystal/First-Program/lib/discordcr/src/discordcr/errors.cr
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
require "http/client/response"
|
||||||
|
require "json"
|
||||||
|
|
||||||
|
module Discord
|
||||||
|
# This exception is raised in `REST#request` when a request fails in general,
|
||||||
|
# without returning a special error response.
|
||||||
|
class StatusException < Exception
|
||||||
|
getter response : HTTP::Client::Response
|
||||||
|
|
||||||
|
def initialize(@response : HTTP::Client::Response)
|
||||||
|
end
|
||||||
|
|
||||||
|
# The status code of the response that caused this exception, for example
|
||||||
|
# 500 or 418.
|
||||||
|
def status_code : Int32
|
||||||
|
@response.status_code
|
||||||
|
end
|
||||||
|
|
||||||
|
# The status message of the response that caused this exception, for example
|
||||||
|
# "Internal Server Error" or "I'm A Teapot".
|
||||||
|
def status_message : String
|
||||||
|
@response.status_message
|
||||||
|
end
|
||||||
|
|
||||||
|
def message
|
||||||
|
"#{@response.status_code} #{@response.status_message}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s(io)
|
||||||
|
io << @response.status_code << " " << @response.status_message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# An API error response.
|
||||||
|
struct APIError
|
||||||
|
JSON.mapping(
|
||||||
|
code: Int32,
|
||||||
|
message: String
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# This exception is raised in `REST#request` when a request fails with an
|
||||||
|
# API error response that has a code and a descriptive message.
|
||||||
|
class CodeException < StatusException
|
||||||
|
getter error : APIError
|
||||||
|
|
||||||
|
def initialize(@response : HTTP::Client::Response, @error : APIError)
|
||||||
|
end
|
||||||
|
|
||||||
|
# The API error code that was returned by Discord, for example 20001 or
|
||||||
|
# 50016.
|
||||||
|
def error_code : Int32
|
||||||
|
@error.code
|
||||||
|
end
|
||||||
|
|
||||||
|
# The API error message that was returned by Discord, for example "Bots
|
||||||
|
# cannot use this endpoint" or "Provided too few or too many messages to
|
||||||
|
# delete. Must provide at least 2 and fewer than 100 messages to delete.".
|
||||||
|
def error_message : String
|
||||||
|
@error.message
|
||||||
|
end
|
||||||
|
|
||||||
|
def message
|
||||||
|
"#{@response.status_code} #{@response.status_message}: Code #{@error.code} - #{@error.message}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s(io)
|
||||||
|
io << @response.status_code << " " << @response.status_message << ": Code " << @error.code << " - " << @error.message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
12
Crystal/First-Program/lib/discordcr/src/discordcr/logger.cr
Normal file
12
Crystal/First-Program/lib/discordcr/src/discordcr/logger.cr
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
require "logger"
|
||||||
|
|
||||||
|
# The logger class is monkey patched to have a property for the IO.
|
||||||
|
class Logger
|
||||||
|
property io
|
||||||
|
end
|
||||||
|
|
||||||
|
module Discord
|
||||||
|
# The built in logger.
|
||||||
|
LOGGER = Logger.new(STDOUT)
|
||||||
|
LOGGER.progname = "discordcr"
|
||||||
|
end
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
require "./converters"
|
||||||
|
|
||||||
|
module Discord
|
||||||
|
enum MessageType : UInt8
|
||||||
|
Default = 0
|
||||||
|
RecipientAdd = 1
|
||||||
|
RecipientRemove = 2
|
||||||
|
Call = 3
|
||||||
|
ChannelNameChange = 4
|
||||||
|
ChannelIconChange = 5
|
||||||
|
ChannelPinnedMessage = 6
|
||||||
|
GuildMemberJoin = 7
|
||||||
|
end
|
||||||
|
|
||||||
|
struct Message
|
||||||
|
JSON.mapping(
|
||||||
|
type: {type: MessageType, converter: MessageTypeConverter},
|
||||||
|
content: String,
|
||||||
|
id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
channel_id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
author: User,
|
||||||
|
timestamp: {type: Time, converter: TimestampConverter},
|
||||||
|
tts: Bool,
|
||||||
|
mention_everyone: Bool,
|
||||||
|
mentions: Array(User),
|
||||||
|
mention_roles: {type: Array(UInt64), converter: SnowflakeArrayConverter},
|
||||||
|
attachments: Array(Attachment),
|
||||||
|
embeds: Array(Embed),
|
||||||
|
pinned: Bool?,
|
||||||
|
reactions: Array(Reaction)?,
|
||||||
|
activity: Activity?
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
enum ActivityType : UInt8
|
||||||
|
Join = 1
|
||||||
|
Spectate = 2
|
||||||
|
Listen = 3
|
||||||
|
JoinRequest = 5
|
||||||
|
end
|
||||||
|
|
||||||
|
struct Activity
|
||||||
|
JSON.mapping(
|
||||||
|
type: ActivityType,
|
||||||
|
party_id: String?
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
enum ChannelType : UInt8
|
||||||
|
GuildText = 0
|
||||||
|
DM = 1
|
||||||
|
Voice = 2
|
||||||
|
GroupDM = 3
|
||||||
|
end
|
||||||
|
|
||||||
|
struct Channel
|
||||||
|
# :nodoc:
|
||||||
|
def initialize(private_channel : PrivateChannel)
|
||||||
|
@id = private_channel.id
|
||||||
|
@type = private_channel.type
|
||||||
|
@recipients = private_channel.recipients
|
||||||
|
@last_message_id = private_channel.last_message_id
|
||||||
|
end
|
||||||
|
|
||||||
|
JSON.mapping(
|
||||||
|
id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
type: {type: ChannelType, converter: ChannelTypeConverter},
|
||||||
|
guild_id: {type: UInt64?, converter: MaybeSnowflakeConverter},
|
||||||
|
name: String?,
|
||||||
|
permission_overwrites: Array(Overwrite)?,
|
||||||
|
topic: String?,
|
||||||
|
last_message_id: {type: UInt64?, converter: MaybeSnowflakeConverter},
|
||||||
|
bitrate: UInt32?,
|
||||||
|
user_limit: UInt32?,
|
||||||
|
recipients: Array(User)?,
|
||||||
|
nsfw: Bool?,
|
||||||
|
icon: Bool?,
|
||||||
|
owner_id: {type: UInt64?, converter: MaybeSnowflakeConverter},
|
||||||
|
application_id: {type: UInt64?, converter: MaybeSnowflakeConverter},
|
||||||
|
position: Int32?,
|
||||||
|
parent_id: {type: UInt64?, converter: MaybeSnowflakeConverter}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct PrivateChannel
|
||||||
|
JSON.mapping(
|
||||||
|
id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
type: {type: ChannelType, converter: ChannelTypeConverter},
|
||||||
|
recipients: Array(User),
|
||||||
|
last_message_id: {type: UInt64?, converter: MaybeSnowflakeConverter}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct Overwrite
|
||||||
|
JSON.mapping(
|
||||||
|
id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
type: String,
|
||||||
|
allow: Permissions,
|
||||||
|
deny: Permissions
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct Reaction
|
||||||
|
JSON.mapping(
|
||||||
|
emoji: ReactionEmoji,
|
||||||
|
count: UInt32,
|
||||||
|
me: Bool
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct ReactionEmoji
|
||||||
|
JSON.mapping(
|
||||||
|
id: {type: UInt64?, converter: MaybeSnowflakeConverter},
|
||||||
|
name: String
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct Embed
|
||||||
|
def initialize(@title : String? = nil, @type : String = "rich",
|
||||||
|
@description : String? = nil, @url : String? = nil,
|
||||||
|
@timestamp : Time? = nil, @colour : UInt32? = nil,
|
||||||
|
@footer : EmbedFooter? = nil, @image : EmbedImage? = nil,
|
||||||
|
@thumbnail : EmbedThumbnail? = nil, @author : EmbedAuthor? = nil,
|
||||||
|
@fields : Array(EmbedField)? = nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
JSON.mapping(
|
||||||
|
title: String?,
|
||||||
|
type: String,
|
||||||
|
description: String?,
|
||||||
|
url: String?,
|
||||||
|
timestamp: {type: Time?, converter: TimestampConverter},
|
||||||
|
colour: {type: UInt32?, key: "color"},
|
||||||
|
footer: EmbedFooter?,
|
||||||
|
image: EmbedImage?,
|
||||||
|
thumbnail: EmbedThumbnail?,
|
||||||
|
video: EmbedVideo?,
|
||||||
|
provider: EmbedProvider?,
|
||||||
|
author: EmbedAuthor?,
|
||||||
|
fields: Array(EmbedField)?
|
||||||
|
)
|
||||||
|
|
||||||
|
{% unless flag?(:correct_english) %}
|
||||||
|
def color
|
||||||
|
colour
|
||||||
|
end
|
||||||
|
{% end %}
|
||||||
|
end
|
||||||
|
|
||||||
|
struct EmbedThumbnail
|
||||||
|
def initialize(@url : String)
|
||||||
|
end
|
||||||
|
|
||||||
|
JSON.mapping(
|
||||||
|
url: String,
|
||||||
|
proxy_url: String?,
|
||||||
|
height: UInt32?,
|
||||||
|
width: UInt32?
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct EmbedVideo
|
||||||
|
JSON.mapping(
|
||||||
|
url: String,
|
||||||
|
height: UInt32,
|
||||||
|
width: UInt32
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct EmbedImage
|
||||||
|
def initialize(@url : String)
|
||||||
|
end
|
||||||
|
|
||||||
|
JSON.mapping(
|
||||||
|
url: String,
|
||||||
|
proxy_url: String?,
|
||||||
|
height: UInt32?,
|
||||||
|
width: UInt32?
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct EmbedProvider
|
||||||
|
JSON.mapping(
|
||||||
|
name: String,
|
||||||
|
url: String?
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct EmbedAuthor
|
||||||
|
def initialize(@name : String? = nil, @url : String? = nil, @icon_url : String? = nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
JSON.mapping(
|
||||||
|
name: String?,
|
||||||
|
url: String?,
|
||||||
|
icon_url: String?,
|
||||||
|
proxy_icon_url: String?
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct EmbedFooter
|
||||||
|
def initialize(@text : String? = nil, @icon_url : String? = nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
JSON.mapping(
|
||||||
|
text: String?,
|
||||||
|
icon_url: String?,
|
||||||
|
proxy_icon_url: String?
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct EmbedField
|
||||||
|
def initialize(@name : String, @value : String, @inline : Bool = false)
|
||||||
|
end
|
||||||
|
|
||||||
|
JSON.mapping(
|
||||||
|
name: String,
|
||||||
|
value: String,
|
||||||
|
inline: Bool
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct Attachment
|
||||||
|
JSON.mapping(
|
||||||
|
id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
filename: String,
|
||||||
|
size: UInt32,
|
||||||
|
url: String,
|
||||||
|
proxy_url: String,
|
||||||
|
height: UInt32?,
|
||||||
|
width: UInt32?
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
require "json"
|
||||||
|
require "time/format"
|
||||||
|
|
||||||
|
module Discord
|
||||||
|
# :nodoc:
|
||||||
|
module TimestampConverter
|
||||||
|
def self.from_json(parser : JSON::PullParser)
|
||||||
|
time_str = parser.read_string
|
||||||
|
|
||||||
|
begin
|
||||||
|
Time::Format.new("%FT%T.%6N%:z").parse(time_str)
|
||||||
|
rescue Time::Format::Error
|
||||||
|
Time::Format.new("%FT%T%:z").parse(time_str)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.to_json(value : Time, builder : JSON::Builder)
|
||||||
|
Time::Format.new("%FT%T.%6N%:z").to_json(value, builder)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# :nodoc:
|
||||||
|
module SnowflakeConverter
|
||||||
|
def self.from_json(parser : JSON::PullParser) : UInt64
|
||||||
|
parser.read_string.to_u64
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.to_json(value : UInt64, builder : JSON::Builder)
|
||||||
|
builder.scalar(value.to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# :nodoc:
|
||||||
|
module MaybeSnowflakeConverter
|
||||||
|
def self.from_json(parser : JSON::PullParser) : UInt64?
|
||||||
|
str = parser.read_string_or_null
|
||||||
|
|
||||||
|
if str
|
||||||
|
str.to_u64
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.to_json(value : UInt64?, builder : JSON::Builder)
|
||||||
|
if value
|
||||||
|
builder.scalar(value.to_s)
|
||||||
|
else
|
||||||
|
builder.null
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# :nodoc:
|
||||||
|
module SnowflakeArrayConverter
|
||||||
|
def self.from_json(parser : JSON::PullParser) : Array(UInt64)
|
||||||
|
Array(String).new(parser).map &.to_u64
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.to_json(value : Array(UInt64), builder : JSON::Builder)
|
||||||
|
value.map(&.to_s).to_json(builder)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# :nodoc:
|
||||||
|
module MessageTypeConverter
|
||||||
|
def self.from_json(parser : JSON::PullParser)
|
||||||
|
if value = parser.read?(UInt8)
|
||||||
|
MessageType.new(value)
|
||||||
|
else
|
||||||
|
raise "Unexpected message type value: #{parser.read_raw}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.to_json(value : MessageType, builder : JSON::Builder)
|
||||||
|
value.to_json(builder)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# :nodoc:
|
||||||
|
module ChannelTypeConverter
|
||||||
|
def self.from_json(parser : JSON::PullParser)
|
||||||
|
if value = parser.read?(UInt8)
|
||||||
|
ChannelType.new(value)
|
||||||
|
else
|
||||||
|
raise "Unexpected channel type value: #{parser.read_raw}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
module Discord::REST
|
||||||
|
# Enum for `parent_id` null significance in
|
||||||
|
# `REST#modify_guild_channel_positions`.
|
||||||
|
enum ChannelParent
|
||||||
|
None
|
||||||
|
Unchanged
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,374 @@
|
|||||||
|
require "./converters"
|
||||||
|
require "./user"
|
||||||
|
require "./channel"
|
||||||
|
require "./guild"
|
||||||
|
|
||||||
|
module Discord
|
||||||
|
module Gateway
|
||||||
|
struct ReadyPayload
|
||||||
|
JSON.mapping(
|
||||||
|
v: UInt8,
|
||||||
|
user: User,
|
||||||
|
private_channels: Array(PrivateChannel),
|
||||||
|
guilds: Array(UnavailableGuild),
|
||||||
|
session_id: String
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct ResumedPayload
|
||||||
|
JSON.mapping(
|
||||||
|
_trace: Array(String)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct IdentifyPacket
|
||||||
|
def initialize(token, properties, large_threshold, compress, shard)
|
||||||
|
@op = Discord::Client::OP_IDENTIFY
|
||||||
|
@d = IdentifyPayload.new(token, properties, large_threshold, compress, shard)
|
||||||
|
end
|
||||||
|
|
||||||
|
JSON.mapping(
|
||||||
|
op: Int32,
|
||||||
|
d: IdentifyPayload
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct IdentifyPayload
|
||||||
|
def initialize(@token, @properties, @compress, @large_threshold, @shard)
|
||||||
|
end
|
||||||
|
|
||||||
|
JSON.mapping({
|
||||||
|
token: String,
|
||||||
|
properties: IdentifyProperties,
|
||||||
|
compress: Bool,
|
||||||
|
large_threshold: Int32,
|
||||||
|
shard: Tuple(Int32, Int32)?,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
struct IdentifyProperties
|
||||||
|
def initialize(@os, @browser, @device, @referrer, @referring_domain)
|
||||||
|
end
|
||||||
|
|
||||||
|
JSON.mapping(
|
||||||
|
os: {key: "$os", type: String},
|
||||||
|
browser: {key: "$browser", type: String},
|
||||||
|
device: {key: "$device", type: String},
|
||||||
|
referrer: {key: "$referrer", type: String},
|
||||||
|
referring_domain: {key: "$referring_domain", type: String}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct ResumePacket
|
||||||
|
def initialize(token, session_id, seq)
|
||||||
|
@op = Discord::Client::OP_RESUME
|
||||||
|
@d = ResumePayload.new(token, session_id, seq)
|
||||||
|
end
|
||||||
|
|
||||||
|
JSON.mapping(
|
||||||
|
op: Int32,
|
||||||
|
d: ResumePayload
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# :nodoc:
|
||||||
|
struct ResumePayload
|
||||||
|
def initialize(@token, @session_id, @seq)
|
||||||
|
end
|
||||||
|
|
||||||
|
JSON.mapping(
|
||||||
|
token: String,
|
||||||
|
session_id: String,
|
||||||
|
seq: Int64
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct StatusUpdatePacket
|
||||||
|
def initialize(status, game, afk, since)
|
||||||
|
@op = Discord::Client::OP_STATUS_UPDATE
|
||||||
|
@d = StatusUpdatePayload.new(status, game, afk, since)
|
||||||
|
end
|
||||||
|
|
||||||
|
JSON.mapping(
|
||||||
|
op: Int32,
|
||||||
|
d: StatusUpdatePayload
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# :nodoc:
|
||||||
|
struct StatusUpdatePayload
|
||||||
|
def initialize(@status, @game, @afk, @since)
|
||||||
|
end
|
||||||
|
|
||||||
|
JSON.mapping(
|
||||||
|
status: {type: String?, emit_null: true},
|
||||||
|
game: {type: GamePlaying?, emit_null: true},
|
||||||
|
afk: Bool,
|
||||||
|
since: {type: Int64, nilable: true, emit_null: true}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct VoiceStateUpdatePacket
|
||||||
|
def initialize(guild_id, channel_id, self_mute, self_deaf)
|
||||||
|
@op = Discord::Client::OP_VOICE_STATE_UPDATE
|
||||||
|
@d = VoiceStateUpdatePayload.new(guild_id, channel_id, self_mute, self_deaf)
|
||||||
|
end
|
||||||
|
|
||||||
|
JSON.mapping(
|
||||||
|
op: Int32,
|
||||||
|
d: VoiceStateUpdatePayload
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# :nodoc:
|
||||||
|
struct VoiceStateUpdatePayload
|
||||||
|
def initialize(@guild_id, @channel_id, @self_mute, @self_deaf)
|
||||||
|
end
|
||||||
|
|
||||||
|
JSON.mapping(
|
||||||
|
guild_id: UInt64,
|
||||||
|
channel_id: {type: UInt64?, emit_null: true},
|
||||||
|
self_mute: Bool,
|
||||||
|
self_deaf: Bool
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct RequestGuildMembersPacket
|
||||||
|
def initialize(guild_id, query, limit)
|
||||||
|
@op = Discord::Client::OP_REQUEST_GUILD_MEMBERS
|
||||||
|
@d = RequestGuildMembersPayload.new(guild_id, query, limit)
|
||||||
|
end
|
||||||
|
|
||||||
|
JSON.mapping(
|
||||||
|
op: Int32,
|
||||||
|
d: RequestGuildMembersPayload
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# :nodoc:
|
||||||
|
struct RequestGuildMembersPayload
|
||||||
|
def initialize(@guild_id, @query, @limit)
|
||||||
|
end
|
||||||
|
|
||||||
|
JSON.mapping(
|
||||||
|
guild_id: UInt64,
|
||||||
|
query: String,
|
||||||
|
limit: Int32
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct HelloPayload
|
||||||
|
JSON.mapping(
|
||||||
|
heartbeat_interval: UInt32,
|
||||||
|
_trace: Array(String)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# This one is special from simply Guild since it also has fields for members
|
||||||
|
# and presences.
|
||||||
|
struct GuildCreatePayload
|
||||||
|
JSON.mapping(
|
||||||
|
id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
name: String,
|
||||||
|
icon: String?,
|
||||||
|
splash: String?,
|
||||||
|
owner_id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
region: String,
|
||||||
|
afk_channel_id: {type: UInt64?, converter: MaybeSnowflakeConverter},
|
||||||
|
afk_timeout: Int32?,
|
||||||
|
verification_level: UInt8,
|
||||||
|
roles: Array(Role),
|
||||||
|
emoji: {type: Array(Emoji), key: "emojis"},
|
||||||
|
features: Array(String),
|
||||||
|
large: Bool,
|
||||||
|
voice_states: Array(VoiceState),
|
||||||
|
unavailable: Bool?,
|
||||||
|
member_count: Int32,
|
||||||
|
members: Array(GuildMember),
|
||||||
|
channels: Array(Channel),
|
||||||
|
presences: Array(Presence),
|
||||||
|
widget_channel_id: {type: UInt64?, converter: MaybeSnowflakeConverter},
|
||||||
|
default_message_notifications: UInt8,
|
||||||
|
explicit_content_filter: UInt8,
|
||||||
|
system_channel_id: {type: UInt64?, converter: MaybeSnowflakeConverter}
|
||||||
|
)
|
||||||
|
|
||||||
|
{% unless flag?(:correct_english) %}
|
||||||
|
def emojis
|
||||||
|
emoji
|
||||||
|
end
|
||||||
|
{% end %}
|
||||||
|
end
|
||||||
|
|
||||||
|
struct GuildDeletePayload
|
||||||
|
JSON.mapping(
|
||||||
|
id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
unavailable: Bool?
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct GuildBanPayload
|
||||||
|
JSON.mapping(
|
||||||
|
user: User,
|
||||||
|
guild_id: {type: UInt64, converter: SnowflakeConverter}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct GuildEmojiUpdatePayload
|
||||||
|
JSON.mapping(
|
||||||
|
guild_id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
emoji: {type: Array(Emoji), key: "emojis"}
|
||||||
|
)
|
||||||
|
|
||||||
|
{% unless flag?(:correct_english) %}
|
||||||
|
def emojis
|
||||||
|
emoji
|
||||||
|
end
|
||||||
|
{% end %}
|
||||||
|
end
|
||||||
|
|
||||||
|
struct GuildIntegrationsUpdatePayload
|
||||||
|
JSON.mapping(
|
||||||
|
guild_id: {type: UInt64, converter: SnowflakeConverter}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct GuildMemberAddPayload
|
||||||
|
JSON.mapping(
|
||||||
|
user: User,
|
||||||
|
nick: String?,
|
||||||
|
roles: {type: Array(UInt64), converter: SnowflakeArrayConverter},
|
||||||
|
joined_at: {type: Time?, converter: TimestampConverter},
|
||||||
|
deaf: Bool,
|
||||||
|
mute: Bool,
|
||||||
|
guild_id: {type: UInt64, converter: SnowflakeConverter}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct GuildMemberUpdatePayload
|
||||||
|
JSON.mapping(
|
||||||
|
user: User,
|
||||||
|
roles: {type: Array(UInt64), converter: SnowflakeArrayConverter},
|
||||||
|
nick: {type: String, nilable: true},
|
||||||
|
guild_id: {type: UInt64, converter: SnowflakeConverter}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct GuildMemberRemovePayload
|
||||||
|
JSON.mapping(
|
||||||
|
user: User,
|
||||||
|
guild_id: {type: UInt64, converter: SnowflakeConverter}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct GuildMembersChunkPayload
|
||||||
|
JSON.mapping(
|
||||||
|
guild_id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
members: Array(GuildMember)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct GuildRolePayload
|
||||||
|
JSON.mapping(
|
||||||
|
guild_id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
role: Role
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct GuildRoleDeletePayload
|
||||||
|
JSON.mapping(
|
||||||
|
guild_id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
role_id: {type: UInt64, converter: SnowflakeConverter}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct MessageReactionPayload
|
||||||
|
JSON.mapping(
|
||||||
|
user_id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
channel_id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
message_id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
emoji: ReactionEmoji
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct MessageReactionRemoveAllPayload
|
||||||
|
JSON.mapping(
|
||||||
|
channel_id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
message_id: {type: UInt64, converter: SnowflakeConverter}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct MessageUpdatePayload
|
||||||
|
JSON.mapping(
|
||||||
|
type: UInt8?,
|
||||||
|
content: String?,
|
||||||
|
id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
channel_id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
author: User?,
|
||||||
|
timestamp: {type: Time?, converter: TimestampConverter},
|
||||||
|
tts: Bool?,
|
||||||
|
mention_everyone: Bool?,
|
||||||
|
mentions: Array(User)?,
|
||||||
|
mention_roles: {type: Array(UInt64)?, converter: SnowflakeArrayConverter},
|
||||||
|
attachments: Array(Attachment)?,
|
||||||
|
embeds: Array(Embed)?,
|
||||||
|
pinned: Bool?
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct MessageDeletePayload
|
||||||
|
JSON.mapping(
|
||||||
|
id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
channel_id: {type: UInt64, converter: SnowflakeConverter}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct MessageDeleteBulkPayload
|
||||||
|
JSON.mapping(
|
||||||
|
ids: {type: Array(UInt64), converter: SnowflakeArrayConverter},
|
||||||
|
channel_id: {type: UInt64, converter: SnowflakeConverter}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct PresenceUpdatePayload
|
||||||
|
JSON.mapping(
|
||||||
|
user: PartialUser,
|
||||||
|
roles: {type: Array(UInt64), converter: SnowflakeArrayConverter},
|
||||||
|
game: GamePlaying?,
|
||||||
|
nick: String?,
|
||||||
|
guild_id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
status: String
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct TypingStartPayload
|
||||||
|
JSON.mapping(
|
||||||
|
channel_id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
user_id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
timestamp: {type: Time, converter: Time::EpochConverter}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct VoiceServerUpdatePayload
|
||||||
|
JSON.mapping(
|
||||||
|
token: String,
|
||||||
|
guild_id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
endpoint: String
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct WebhooksUpdatePayload
|
||||||
|
JSON.mapping(
|
||||||
|
guild_id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
channel_id: {type: UInt64, converter: SnowflakeConverter}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct ChannelPinsUpdatePayload
|
||||||
|
JSON.mapping(
|
||||||
|
last_pin_timestamp: {type: Time, converter: TimestampConverter},
|
||||||
|
channel_id: {type: UInt64, converter: SnowflakeConverter}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
require "./converters"
|
||||||
|
require "./voice"
|
||||||
|
|
||||||
|
module Discord
|
||||||
|
struct Guild
|
||||||
|
# :nodoc:
|
||||||
|
def initialize(payload : Gateway::GuildCreatePayload)
|
||||||
|
@id = payload.id
|
||||||
|
@name = payload.name
|
||||||
|
@icon = payload.icon
|
||||||
|
@splash = payload.splash
|
||||||
|
@owner_id = payload.owner_id
|
||||||
|
@region = payload.region
|
||||||
|
@afk_channel_id = payload.afk_channel_id
|
||||||
|
@afk_timeout = payload.afk_timeout
|
||||||
|
@verification_level = payload.verification_level
|
||||||
|
@roles = payload.roles
|
||||||
|
@emoji = payload.emoji
|
||||||
|
@features = payload.features
|
||||||
|
@widget_channel_id = payload.widget_channel_id
|
||||||
|
@default_message_notifications = payload.default_message_notifications
|
||||||
|
@explicit_content_filter = payload.explicit_content_filter
|
||||||
|
@system_channel_id = payload.system_channel_id
|
||||||
|
end
|
||||||
|
|
||||||
|
JSON.mapping(
|
||||||
|
id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
name: String,
|
||||||
|
icon: String?,
|
||||||
|
splash: String?,
|
||||||
|
owner_id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
region: String,
|
||||||
|
afk_channel_id: {type: UInt64?, converter: MaybeSnowflakeConverter},
|
||||||
|
afk_timeout: Int32?,
|
||||||
|
embed_enabled: Bool?,
|
||||||
|
embed_channel_id: {type: UInt64?, converter: MaybeSnowflakeConverter},
|
||||||
|
verification_level: UInt8,
|
||||||
|
roles: Array(Role),
|
||||||
|
emoji: {type: Array(Emoji), key: "emojis"},
|
||||||
|
features: Array(String),
|
||||||
|
widget_enabled: {type: Bool, nilable: true},
|
||||||
|
widget_channel_id: {type: UInt64?, converter: MaybeSnowflakeConverter},
|
||||||
|
default_message_notifications: UInt8,
|
||||||
|
explicit_content_filter: UInt8,
|
||||||
|
system_channel_id: {type: UInt64?, converter: MaybeSnowflakeConverter}
|
||||||
|
)
|
||||||
|
|
||||||
|
{% unless flag?(:correct_english) %}
|
||||||
|
def emojis
|
||||||
|
emoji
|
||||||
|
end
|
||||||
|
{% end %}
|
||||||
|
end
|
||||||
|
|
||||||
|
struct UnavailableGuild
|
||||||
|
JSON.mapping(
|
||||||
|
id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
unavailable: Bool
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct GuildEmbed
|
||||||
|
JSON.mapping(
|
||||||
|
enabled: Bool,
|
||||||
|
channel_id: {type: UInt64, converter: SnowflakeConverter}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct GuildMember
|
||||||
|
# :nodoc:
|
||||||
|
def initialize(payload : Gateway::GuildMemberAddPayload | GuildMember, roles : Array(UInt64), nick : String?)
|
||||||
|
initialize(payload)
|
||||||
|
@nick = nick
|
||||||
|
@roles = roles
|
||||||
|
end
|
||||||
|
|
||||||
|
# :nodoc:
|
||||||
|
def initialize(payload : Gateway::GuildMemberAddPayload | GuildMember)
|
||||||
|
@user = payload.user
|
||||||
|
@nick = payload.nick
|
||||||
|
@roles = payload.roles
|
||||||
|
@joined_at = payload.joined_at
|
||||||
|
@deaf = payload.deaf
|
||||||
|
@mute = payload.mute
|
||||||
|
end
|
||||||
|
|
||||||
|
# :nodoc:
|
||||||
|
def initialize(payload : Gateway::PresenceUpdatePayload)
|
||||||
|
@user = User.new(payload.user)
|
||||||
|
@nick = payload.nick
|
||||||
|
@roles = payload.roles
|
||||||
|
# Presence updates have no joined_at or deaf/mute, thanks Discord
|
||||||
|
end
|
||||||
|
|
||||||
|
JSON.mapping(
|
||||||
|
user: User,
|
||||||
|
nick: String?,
|
||||||
|
roles: {type: Array(UInt64), converter: SnowflakeArrayConverter},
|
||||||
|
joined_at: {type: Time?, converter: TimestampConverter},
|
||||||
|
deaf: Bool?,
|
||||||
|
mute: Bool?
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct Integration
|
||||||
|
JSON.mapping(
|
||||||
|
id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
name: String,
|
||||||
|
type: String,
|
||||||
|
enabled: Bool,
|
||||||
|
syncing: Bool,
|
||||||
|
role_id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
expire_behaviour: {type: UInt8, key: "expire_behavior"},
|
||||||
|
expire_grace_period: Int32,
|
||||||
|
user: User,
|
||||||
|
account: IntegrationAccount,
|
||||||
|
synced_at: {type: Time, converter: Time::EpochConverter}
|
||||||
|
)
|
||||||
|
|
||||||
|
{% unless flag?(:correct_english) %}
|
||||||
|
def expire_behavior
|
||||||
|
expire_behaviour
|
||||||
|
end
|
||||||
|
{% end %}
|
||||||
|
end
|
||||||
|
|
||||||
|
struct IntegrationAccount
|
||||||
|
JSON.mapping(
|
||||||
|
id: String,
|
||||||
|
name: String
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct Emoji
|
||||||
|
JSON.mapping(
|
||||||
|
id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
name: String,
|
||||||
|
roles: {type: Array(UInt64), converter: SnowflakeArrayConverter},
|
||||||
|
require_colons: Bool,
|
||||||
|
managed: Bool
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct Role
|
||||||
|
JSON.mapping(
|
||||||
|
id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
name: String,
|
||||||
|
permissions: Permissions,
|
||||||
|
colour: {type: UInt32, key: "color"},
|
||||||
|
hoist: Bool,
|
||||||
|
position: Int32,
|
||||||
|
managed: Bool,
|
||||||
|
mentionable: Bool
|
||||||
|
)
|
||||||
|
|
||||||
|
{% unless flag?(:correct_english) %}
|
||||||
|
def color
|
||||||
|
colour
|
||||||
|
end
|
||||||
|
{% end %}
|
||||||
|
end
|
||||||
|
|
||||||
|
struct GuildBan
|
||||||
|
JSON.mapping(
|
||||||
|
user: User,
|
||||||
|
reason: String?
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct GamePlaying
|
||||||
|
def initialize(@name = nil, @type = nil, @url = nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
JSON.mapping(
|
||||||
|
name: String?,
|
||||||
|
type: Int64? | String?,
|
||||||
|
url: String?
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct Presence
|
||||||
|
JSON.mapping(
|
||||||
|
user: PartialUser,
|
||||||
|
game: GamePlaying?,
|
||||||
|
status: String
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
require "./converters"
|
||||||
|
require "./user"
|
||||||
|
|
||||||
|
module Discord
|
||||||
|
struct Invite
|
||||||
|
JSON.mapping(
|
||||||
|
code: String,
|
||||||
|
guild: InviteGuild,
|
||||||
|
channel: InviteChannel
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct InviteMetadata
|
||||||
|
JSON.mapping(
|
||||||
|
code: String,
|
||||||
|
guild: InviteGuild,
|
||||||
|
channel: InviteChannel,
|
||||||
|
inviter: User,
|
||||||
|
users: UInt32,
|
||||||
|
max_uses: UInt32,
|
||||||
|
max_age: UInt32,
|
||||||
|
temporary: Bool,
|
||||||
|
created_at: {type: Time, converter: TimestampConverter},
|
||||||
|
revoked: Bool
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct InviteGuild
|
||||||
|
JSON.mapping(
|
||||||
|
id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
name: String,
|
||||||
|
splash_hash: String?
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct InviteChannel
|
||||||
|
JSON.mapping(
|
||||||
|
id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
name: String,
|
||||||
|
type: UInt8
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
require "./converters"
|
||||||
|
require "./user"
|
||||||
|
|
||||||
|
module Discord
|
||||||
|
# An OAuth2 application, as registered with Discord, that can hold
|
||||||
|
# information about a `Client`'s associated bot user account and owner,
|
||||||
|
# among other OAuth2 properties.
|
||||||
|
struct OAuth2Application
|
||||||
|
JSON.mapping({
|
||||||
|
id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
name: String,
|
||||||
|
icon: String?,
|
||||||
|
description: String?,
|
||||||
|
rpc_origins: Array(String)?,
|
||||||
|
bot_public: Bool,
|
||||||
|
bot_require_code_grant: Bool,
|
||||||
|
owner: User,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
module Discord
|
||||||
|
@[Flags]
|
||||||
|
enum Permissions : UInt64
|
||||||
|
CreateInstantInvite = 1
|
||||||
|
KickMembers = 1 << 1
|
||||||
|
BanMembers = 1 << 2
|
||||||
|
Administrator = 1 << 3
|
||||||
|
ManageChannels = 1 << 4
|
||||||
|
ManageGuild = 1 << 5
|
||||||
|
AddReactions = 1 << 6
|
||||||
|
ReadMessages = 1 << 10
|
||||||
|
SendMessages = 1 << 11
|
||||||
|
SendTTSMessages = 1 << 12
|
||||||
|
ManageMessages = 1 << 13
|
||||||
|
EmbedLinks = 1 << 14
|
||||||
|
AttachFiles = 1 << 15
|
||||||
|
ReadMessageHistory = 1 << 16
|
||||||
|
MentionEveryone = 1 << 17
|
||||||
|
UseExternalEmojis = 1 << 18
|
||||||
|
Connect = 1 << 20
|
||||||
|
Speak = 1 << 21
|
||||||
|
MuteMembers = 1 << 22
|
||||||
|
DeafenMembers = 1 << 23
|
||||||
|
MoveMembers = 1 << 24
|
||||||
|
UseVAD = 1 << 25
|
||||||
|
ChangeNickname = 1 << 26
|
||||||
|
ManageNicknames = 1 << 27
|
||||||
|
ManageRoles = 1 << 28
|
||||||
|
ManageWebhooks = 1 << 29
|
||||||
|
ManageEmojis = 1 << 30
|
||||||
|
|
||||||
|
def self.new(pull : JSON::PullParser)
|
||||||
|
# see https://github.com/crystal-lang/crystal/issues/3448
|
||||||
|
# #from_value errors
|
||||||
|
Permissions.new(pull.read_int.to_u64)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
require "./converters"
|
||||||
|
|
||||||
|
module Discord
|
||||||
|
module REST
|
||||||
|
# A response to the Get Gateway REST API call.
|
||||||
|
struct GatewayResponse
|
||||||
|
JSON.mapping(
|
||||||
|
url: String
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# A response to the Get Gateway Bot REST API call.
|
||||||
|
struct GatewayBotResponse
|
||||||
|
JSON.mapping(
|
||||||
|
url: String,
|
||||||
|
shards: Int32
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# A response to the Get Guild Prune Count REST API call.
|
||||||
|
struct PruneCountResponse
|
||||||
|
JSON.mapping(
|
||||||
|
pruned: UInt32
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# A response to the Get Guild Vanity URL REST API call.
|
||||||
|
struct GuildVanityURLResponse
|
||||||
|
JSON.mapping(
|
||||||
|
code: String
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# A request payload to rearrange channels in a `Guild` by a REST API call.
|
||||||
|
struct ModifyChannelPositionPayload
|
||||||
|
def initialize(@id : UInt64, @position : Int32,
|
||||||
|
@parent_id : UInt64 | ChannelParent = ChannelParent::Unchanged,
|
||||||
|
@lock_permissions : Bool? = nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_json(builder : JSON::Builder)
|
||||||
|
builder.object do
|
||||||
|
builder.field("id") do
|
||||||
|
SnowflakeConverter.to_json(@id, builder)
|
||||||
|
end
|
||||||
|
|
||||||
|
builder.field("position", @position)
|
||||||
|
|
||||||
|
case parent = @parent_id
|
||||||
|
when UInt64
|
||||||
|
SnowflakeConverter.to_json(parent, builder)
|
||||||
|
when ChannelParent::None
|
||||||
|
builder.field("parent_id", nil)
|
||||||
|
when ChannelParent::Unchanged
|
||||||
|
# no field
|
||||||
|
end
|
||||||
|
|
||||||
|
builder.field("lock_permissions", @lock_permissions) unless @lock_permissions.nil?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
require "./converters"
|
||||||
|
|
||||||
|
module Discord
|
||||||
|
struct User
|
||||||
|
# :nodoc:
|
||||||
|
def initialize(partial : PartialUser)
|
||||||
|
@username = partial.username.not_nil!
|
||||||
|
@id = partial.id
|
||||||
|
@discriminator = partial.discriminator.not_nil!
|
||||||
|
@avatar = partial.avatar
|
||||||
|
@email = partial.email
|
||||||
|
@bot = partial.bot
|
||||||
|
end
|
||||||
|
|
||||||
|
JSON.mapping(
|
||||||
|
username: String,
|
||||||
|
id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
discriminator: String,
|
||||||
|
avatar: String?,
|
||||||
|
email: String?,
|
||||||
|
bot: Bool?,
|
||||||
|
mfa_enabled: Bool?,
|
||||||
|
verified: Bool?
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct PartialUser
|
||||||
|
JSON.mapping(
|
||||||
|
username: String?,
|
||||||
|
id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
discriminator: String?,
|
||||||
|
avatar: String?,
|
||||||
|
email: String?,
|
||||||
|
bot: Bool?
|
||||||
|
)
|
||||||
|
|
||||||
|
def full? : Bool
|
||||||
|
!@username.nil? && !@discriminator.nil? && !@avatar.nil?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
struct UserGuild
|
||||||
|
JSON.mapping(
|
||||||
|
id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
name: String,
|
||||||
|
icon: String?,
|
||||||
|
owner: Bool,
|
||||||
|
permissions: Permissions
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct Connection
|
||||||
|
JSON.mapping(
|
||||||
|
id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
name: String,
|
||||||
|
type: String,
|
||||||
|
revoked: Bool
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
require "./converters"
|
||||||
|
|
||||||
|
module Discord
|
||||||
|
struct VoiceState
|
||||||
|
JSON.mapping(
|
||||||
|
guild_id: {type: UInt64?, converter: MaybeSnowflakeConverter},
|
||||||
|
channel_id: {type: UInt64?, converter: MaybeSnowflakeConverter},
|
||||||
|
user_id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
session_id: String,
|
||||||
|
deaf: Bool,
|
||||||
|
mute: Bool,
|
||||||
|
self_deaf: Bool,
|
||||||
|
self_mute: Bool,
|
||||||
|
suppress: Bool
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct VoiceRegion
|
||||||
|
JSON.mapping(
|
||||||
|
id: String,
|
||||||
|
name: String,
|
||||||
|
sample_hostname: String,
|
||||||
|
sample_port: UInt16,
|
||||||
|
custom: Bool?,
|
||||||
|
vip: Bool,
|
||||||
|
optimal: Bool
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
require "./converters"
|
||||||
|
|
||||||
|
module Discord
|
||||||
|
# :nodoc:
|
||||||
|
module VWS
|
||||||
|
struct IdentifyPacket
|
||||||
|
def initialize(server_id, user_id, session_id, token)
|
||||||
|
@op = Discord::VoiceClient::OP_IDENTIFY
|
||||||
|
@d = IdentifyPayload.new(server_id, user_id, session_id, token)
|
||||||
|
end
|
||||||
|
|
||||||
|
JSON.mapping(
|
||||||
|
op: Int32,
|
||||||
|
d: IdentifyPayload
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct IdentifyPayload
|
||||||
|
def initialize(@server_id, @user_id, @session_id, @token)
|
||||||
|
end
|
||||||
|
|
||||||
|
JSON.mapping(
|
||||||
|
server_id: UInt64,
|
||||||
|
user_id: UInt64,
|
||||||
|
session_id: String,
|
||||||
|
token: String
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct SelectProtocolPacket
|
||||||
|
def initialize(protocol, data)
|
||||||
|
@op = Discord::VoiceClient::OP_SELECT_PROTOCOL
|
||||||
|
@d = SelectProtocolPayload.new(protocol, data)
|
||||||
|
end
|
||||||
|
|
||||||
|
JSON.mapping(
|
||||||
|
op: Int32,
|
||||||
|
d: SelectProtocolPayload
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct SelectProtocolPayload
|
||||||
|
def initialize(@protocol, @data)
|
||||||
|
end
|
||||||
|
|
||||||
|
JSON.mapping(
|
||||||
|
protocol: String,
|
||||||
|
data: ProtocolData
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct ProtocolData
|
||||||
|
def initialize(@address, @port, @mode)
|
||||||
|
end
|
||||||
|
|
||||||
|
JSON.mapping(
|
||||||
|
address: String,
|
||||||
|
port: UInt16,
|
||||||
|
mode: String
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct ReadyPayload
|
||||||
|
JSON.mapping(
|
||||||
|
ssrc: Int32,
|
||||||
|
port: Int32,
|
||||||
|
modes: Array(String),
|
||||||
|
heartbeat_interval: Int32
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct SessionDescriptionPayload
|
||||||
|
JSON.mapping(
|
||||||
|
secret_key: Array(UInt8)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct SpeakingPacket
|
||||||
|
def initialize(speaking, delay)
|
||||||
|
@op = Discord::VoiceClient::OP_SPEAKING
|
||||||
|
@d = SpeakingPayload.new(speaking, delay)
|
||||||
|
end
|
||||||
|
|
||||||
|
JSON.mapping(
|
||||||
|
op: Int32,
|
||||||
|
d: SpeakingPayload
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct SpeakingPayload
|
||||||
|
def initialize(@speaking, @delay)
|
||||||
|
end
|
||||||
|
|
||||||
|
JSON.mapping(
|
||||||
|
speaking: Bool,
|
||||||
|
delay: Int32
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct HelloPayload
|
||||||
|
JSON.mapping(
|
||||||
|
heartbeat_interval: Int32
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
require "./converters"
|
||||||
|
require "./user"
|
||||||
|
|
||||||
|
module Discord
|
||||||
|
struct Webhook
|
||||||
|
JSON.mapping(
|
||||||
|
id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
guild_id: {type: UInt64?, converter: SnowflakeConverter},
|
||||||
|
channel_id: {type: UInt64, converter: SnowflakeConverter},
|
||||||
|
user: User?,
|
||||||
|
name: String,
|
||||||
|
avatar: String?,
|
||||||
|
token: String
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
129
Crystal/First-Program/lib/discordcr/src/discordcr/mention.cr
Normal file
129
Crystal/First-Program/lib/discordcr/src/discordcr/mention.cr
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
module Discord::Mention
|
||||||
|
record User, id : UInt64, start : Int32, size : Int32
|
||||||
|
|
||||||
|
record Role, id : UInt64, start : Int32, size : Int32
|
||||||
|
|
||||||
|
record Channel, id : UInt64, start : Int32, size : Int32
|
||||||
|
|
||||||
|
record Emoji, animated : Bool, name : String, id : UInt64, start : Int32, size : Int32
|
||||||
|
|
||||||
|
record Everyone, start : Int32 do
|
||||||
|
def size
|
||||||
|
9
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
record Here, start : Int32 do
|
||||||
|
def size
|
||||||
|
5
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
alias MentionType = User | Role | Channel | Emoji | Everyone | Here
|
||||||
|
|
||||||
|
# Returns an array of mentions found in a string
|
||||||
|
def self.parse(string : String)
|
||||||
|
Parser.new(string).parse
|
||||||
|
end
|
||||||
|
|
||||||
|
# Parses a string for mentions, yielding for each mention found
|
||||||
|
def self.parse(string : String, &block : MentionType ->)
|
||||||
|
Parser.new(string).parse(&block)
|
||||||
|
end
|
||||||
|
|
||||||
|
# :nodoc:
|
||||||
|
class Parser
|
||||||
|
def initialize(@string : String)
|
||||||
|
@reader = Char::Reader.new string
|
||||||
|
end
|
||||||
|
|
||||||
|
delegate has_next?, pos, current_char, next_char, peek_next_char, to: @reader
|
||||||
|
|
||||||
|
def parse(&block : MentionType ->)
|
||||||
|
while has_next?
|
||||||
|
start = pos
|
||||||
|
animated = false
|
||||||
|
|
||||||
|
case current_char
|
||||||
|
when '<'
|
||||||
|
case next_char
|
||||||
|
when '@'
|
||||||
|
case peek_next_char
|
||||||
|
when '&'
|
||||||
|
next_char # Skip role mention indicator
|
||||||
|
|
||||||
|
if next_char.ascii_number?
|
||||||
|
snowflake = scan_snowflake(pos)
|
||||||
|
yield Role.new(snowflake, start, pos - start) if has_next? && current_char == '>'
|
||||||
|
end
|
||||||
|
when .ascii_number?, '!'
|
||||||
|
next_char # Skip mention indicator
|
||||||
|
next_char if current_char == '!' # Skip optional nickname indicator
|
||||||
|
|
||||||
|
if current_char.ascii_number?
|
||||||
|
snowflake = scan_snowflake(pos)
|
||||||
|
yield User.new(snowflake, start, pos - start + 1) if current_char == '>'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
when '#'
|
||||||
|
next_char # Skip channel mention indicator
|
||||||
|
|
||||||
|
if peek_next_char.ascii_number?
|
||||||
|
snowflake = scan_snowflake(pos)
|
||||||
|
yield Channel.new(snowflake, start, pos - start + 1) if current_char == '>'
|
||||||
|
end
|
||||||
|
when ':', 'a'
|
||||||
|
if current_char == 'a'
|
||||||
|
next unless peek_next_char == ':'
|
||||||
|
animated = true
|
||||||
|
next_char
|
||||||
|
end
|
||||||
|
next_char
|
||||||
|
|
||||||
|
name = scan_word(pos)
|
||||||
|
if current_char == ':' && peek_next_char.ascii_number?
|
||||||
|
next_char
|
||||||
|
snowflake = scan_snowflake(pos)
|
||||||
|
yield Emoji.new(animated, name, snowflake, start, pos - start + 1) if current_char == '>'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
when '@'
|
||||||
|
word = scan_word(pos)
|
||||||
|
case word
|
||||||
|
when "@everyone"
|
||||||
|
yield Everyone.new(start)
|
||||||
|
when "@here"
|
||||||
|
yield Here.new(start)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
next_char
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse
|
||||||
|
results = [] of MentionType
|
||||||
|
parse { |mention| results << mention }
|
||||||
|
results
|
||||||
|
end
|
||||||
|
|
||||||
|
private def scan_snowflake(start)
|
||||||
|
while next_char.ascii_number?
|
||||||
|
# Nothing to do
|
||||||
|
end
|
||||||
|
@string[start..pos - 1].to_u64
|
||||||
|
end
|
||||||
|
|
||||||
|
private def scan_word(start)
|
||||||
|
while has_next?
|
||||||
|
case next_char
|
||||||
|
when .ascii_letter?, .ascii_number?
|
||||||
|
# Nothing to do
|
||||||
|
else
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
@string[start..pos - 1]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
1680
Crystal/First-Program/lib/discordcr/src/discordcr/rest.cr
Normal file
1680
Crystal/First-Program/lib/discordcr/src/discordcr/rest.cr
Normal file
File diff suppressed because it is too large
Load Diff
22
Crystal/First-Program/lib/discordcr/src/discordcr/sodium.cr
Normal file
22
Crystal/First-Program/lib/discordcr/src/discordcr/sodium.cr
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
module Discord
|
||||||
|
# Bindings to libsodium. These aren't intended to be general bindings, just
|
||||||
|
# for the specific xsalsa20poly1305 encryption Discord uses.
|
||||||
|
@[Link("sodium")]
|
||||||
|
lib Sodium
|
||||||
|
# Encrypt something using xsalsa20poly1305
|
||||||
|
fun crypto_secretbox_xsalsa20poly1305(c : UInt8*, message : UInt8*,
|
||||||
|
mlen : UInt64, nonce : UInt8*,
|
||||||
|
key : UInt8*) : LibC::Int
|
||||||
|
|
||||||
|
# Decrypt something using xsalsa20poly1305 ("open a secretbox")
|
||||||
|
fun crypto_secretbox_xsalsa20poly1305_open(message : UInt8*, c : UInt8*,
|
||||||
|
mlen : UInt64, nonce : UInt8*,
|
||||||
|
key : UInt8*) : LibC::Int
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
fun crypto_secretbox_xsalsa20poly1305_keybytes : LibC::SizeT # Key size in bytes
|
||||||
|
fun crypto_secretbox_xsalsa20poly1305_noncebytes : LibC::SizeT # Nonce size in bytes
|
||||||
|
fun crypto_secretbox_xsalsa20poly1305_zerobytes : LibC::SizeT # Zero bytes before a plaintext
|
||||||
|
fun crypto_secretbox_xsalsa20poly1305_boxzerobytes : LibC::SizeT # Zero bytes before a ciphertext
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
module Discord
|
||||||
|
VERSION = "0.3.0"
|
||||||
|
end
|
||||||
298
Crystal/First-Program/lib/discordcr/src/discordcr/voice.cr
Normal file
298
Crystal/First-Program/lib/discordcr/src/discordcr/voice.cr
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
require "uri"
|
||||||
|
|
||||||
|
require "./mappings/gateway"
|
||||||
|
require "./mappings/vws"
|
||||||
|
require "./websocket"
|
||||||
|
require "./sodium"
|
||||||
|
|
||||||
|
module Discord
|
||||||
|
class VoiceClient
|
||||||
|
UDP_PROTOCOL = "udp"
|
||||||
|
|
||||||
|
# The mode that tells Discord we want to send encrypted audio
|
||||||
|
ENCRYPTED_MODE = "xsalsa20_poly1305"
|
||||||
|
|
||||||
|
OP_IDENTIFY = 0
|
||||||
|
OP_SELECT_PROTOCOL = 1
|
||||||
|
OP_READY = 2
|
||||||
|
OP_HEARTBEAT = 3
|
||||||
|
OP_SESSION_DESCRIPTION = 4
|
||||||
|
OP_SPEAKING = 5
|
||||||
|
OP_HELLO = 8
|
||||||
|
|
||||||
|
# The heartbeat is the same every time, so it can be a constant
|
||||||
|
HEARTBEAT_JSON = {op: OP_HEARTBEAT, d: nil}.to_json
|
||||||
|
|
||||||
|
@udp : VoiceUDP
|
||||||
|
|
||||||
|
@sequence : UInt16 = 0_u16
|
||||||
|
@time : UInt32 = 0_u32
|
||||||
|
|
||||||
|
@endpoint : String
|
||||||
|
@server_id : UInt64
|
||||||
|
@session_id : String
|
||||||
|
@token : String
|
||||||
|
|
||||||
|
@heartbeat_interval : Int32?
|
||||||
|
|
||||||
|
# Creates a new voice client. The *payload* should be a payload received
|
||||||
|
# from Discord as part of a VOICE_SERVER_UPDATE dispatch, received after
|
||||||
|
# sending a voice state update (gateway op 4) packet. The *session* should
|
||||||
|
# be the session currently in use by the gateway client on which the
|
||||||
|
# aforementioned dispatch was received, and the *user_id* should be the
|
||||||
|
# user ID of the account on which the voice client is created. (It is
|
||||||
|
# received as part of the gateway READY dispatch, for example)
|
||||||
|
def initialize(payload : Discord::Gateway::VoiceServerUpdatePayload,
|
||||||
|
session : Discord::Gateway::Session, @user_id : UInt64)
|
||||||
|
@endpoint = payload.endpoint.gsub(":80", "")
|
||||||
|
|
||||||
|
@server_id = payload.guild_id
|
||||||
|
@session_id = session.session_id
|
||||||
|
@token = payload.token
|
||||||
|
|
||||||
|
@websocket = Discord::WebSocket.new(
|
||||||
|
host: @endpoint,
|
||||||
|
path: "/",
|
||||||
|
port: 443,
|
||||||
|
tls: true
|
||||||
|
)
|
||||||
|
|
||||||
|
@websocket.on_message(&->on_message(Discord::WebSocket::Packet))
|
||||||
|
@websocket.on_close(&->on_close(String))
|
||||||
|
|
||||||
|
@udp = VoiceUDP.new
|
||||||
|
end
|
||||||
|
|
||||||
|
# Initiates the connection process and blocks forever afterwards.
|
||||||
|
def run
|
||||||
|
spawn { heartbeat_loop }
|
||||||
|
@websocket.run
|
||||||
|
end
|
||||||
|
|
||||||
|
# Closes the VWS connection, in effect disconnecting from voice.
|
||||||
|
delegate close, to: @websocket
|
||||||
|
|
||||||
|
# Sets the handler that should be run once the voice client has connected
|
||||||
|
# successfully.
|
||||||
|
def on_ready(&@ready_handler : ->)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Sends a packet to indicate to Discord whether or not we are speaking
|
||||||
|
# right now
|
||||||
|
def send_speaking(speaking : Bool, delay : Int32 = 0)
|
||||||
|
packet = VWS::SpeakingPacket.new(speaking, delay)
|
||||||
|
@websocket.send(packet.to_json)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Plays a single opus packet
|
||||||
|
def play_opus(buf : Bytes)
|
||||||
|
increment_packet_metadata
|
||||||
|
@udp.send_audio(buf, @sequence, @time)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Increment sequence and time
|
||||||
|
private def increment_packet_metadata
|
||||||
|
@sequence += 1
|
||||||
|
@time += 960
|
||||||
|
end
|
||||||
|
|
||||||
|
private def heartbeat_loop
|
||||||
|
loop do
|
||||||
|
if @heartbeat_interval
|
||||||
|
@websocket.send(HEARTBEAT_JSON)
|
||||||
|
sleep @heartbeat_interval.not_nil!.milliseconds
|
||||||
|
else
|
||||||
|
sleep 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private def on_message(packet : Discord::WebSocket::Packet)
|
||||||
|
LOGGER.debug("VWS packet received: #{packet} #{packet.data.to_s}")
|
||||||
|
|
||||||
|
case packet.opcode
|
||||||
|
when OP_READY
|
||||||
|
payload = VWS::ReadyPayload.from_json(packet.data)
|
||||||
|
handle_ready(payload)
|
||||||
|
when OP_SESSION_DESCRIPTION
|
||||||
|
payload = VWS::SessionDescriptionPayload.from_json(packet.data)
|
||||||
|
handle_session_description(payload)
|
||||||
|
when OP_HELLO
|
||||||
|
payload = VWS::HelloPayload.from_json(packet.data)
|
||||||
|
handle_hello(payload)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private def on_close(message : String)
|
||||||
|
if message.bytesize < 2
|
||||||
|
LOGGER.warn("VWS closed with data: #{message.bytes}")
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
code = IO::Memory.new(message.byte_slice(0, 2)).read_bytes(UInt16, IO::ByteFormat::BigEndian)
|
||||||
|
reason = message.byte_slice(2, message.bytesize - 2)
|
||||||
|
LOGGER.warn("VWS closed with code #{code}, reason: #{reason}")
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
private def handle_ready(payload : VWS::ReadyPayload)
|
||||||
|
# We get a new heartbeat interval here that replaces the old one
|
||||||
|
@heartbeat_interval = payload.heartbeat_interval
|
||||||
|
udp_connect(payload.port.to_u32, payload.ssrc.to_u32)
|
||||||
|
end
|
||||||
|
|
||||||
|
private def udp_connect(port, ssrc)
|
||||||
|
@udp.connect(@endpoint, port, ssrc)
|
||||||
|
@udp.send_discovery
|
||||||
|
ip, port = @udp.receive_discovery_reply
|
||||||
|
send_select_protocol(UDP_PROTOCOL, ip, port, ENCRYPTED_MODE)
|
||||||
|
end
|
||||||
|
|
||||||
|
private def send_identify(server_id, user_id, session_id, token)
|
||||||
|
packet = VWS::IdentifyPacket.new(server_id, user_id, session_id, token)
|
||||||
|
@websocket.send(packet.to_json)
|
||||||
|
end
|
||||||
|
|
||||||
|
private def send_select_protocol(protocol, address, port, mode)
|
||||||
|
data = VWS::ProtocolData.new(address, port, mode)
|
||||||
|
packet = VWS::SelectProtocolPacket.new(protocol, data)
|
||||||
|
@websocket.send(packet.to_json)
|
||||||
|
end
|
||||||
|
|
||||||
|
private def handle_session_description(payload : VWS::SessionDescriptionPayload)
|
||||||
|
@udp.secret_key = Bytes.new(payload.secret_key.to_unsafe, payload.secret_key.size)
|
||||||
|
|
||||||
|
# Once the secret key has been received, we are ready to send audio data.
|
||||||
|
# Notify the user of this
|
||||||
|
spawn { @ready_handler.try(&.call) }
|
||||||
|
end
|
||||||
|
|
||||||
|
private def handle_hello(payload : VWS::HelloPayload)
|
||||||
|
@heartbeat_interval = payload.heartbeat_interval
|
||||||
|
send_identify(@server_id, @user_id, @session_id, @token)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Client for Discord's voice UDP protocol, on which the actual audio data is
|
||||||
|
# sent. There should be no reason to manually use this class: use
|
||||||
|
# `VoiceClient` instead which uses this class internally.
|
||||||
|
class VoiceUDP
|
||||||
|
@secret_key : Bytes?
|
||||||
|
property secret_key
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
@socket = UDPSocket.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def connect(endpoint : String, port : UInt32, ssrc : UInt32)
|
||||||
|
@ssrc = ssrc
|
||||||
|
@socket.connect(endpoint, port)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Sends a discovery packet to Discord, telling them that we want to know our
|
||||||
|
# IP so we can select the protocol on the VWS
|
||||||
|
def send_discovery
|
||||||
|
io = IO::Memory.new(70)
|
||||||
|
|
||||||
|
io.write_bytes(@ssrc.not_nil!, IO::ByteFormat::BigEndian)
|
||||||
|
io.write(Bytes.new(70 - sizeof(UInt32), 0_u8))
|
||||||
|
|
||||||
|
@socket.write(io.to_slice)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Awaits a response to the discovery request and returns our local IP and
|
||||||
|
# port once the response is received
|
||||||
|
def receive_discovery_reply : {String, UInt16}
|
||||||
|
buf = Bytes.new(70)
|
||||||
|
@socket.receive(buf)
|
||||||
|
io = IO::Memory.new(buf)
|
||||||
|
|
||||||
|
io.seek(4) # The first four bytes are just the SSRC again, we don't care about that
|
||||||
|
ip = io.read_string(64).delete("\0")
|
||||||
|
port = io.read_bytes(UInt16, IO::ByteFormat::BigEndian)
|
||||||
|
|
||||||
|
{ip, port}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Sends 20 ms of opus audio data to Discord, with the specified sequence and
|
||||||
|
# time (used on the receiving client to synchronise packets)
|
||||||
|
def send_audio(buf, sequence, time)
|
||||||
|
header = create_header(sequence, time)
|
||||||
|
|
||||||
|
buf = encrypt_audio(header, buf)
|
||||||
|
|
||||||
|
new_buf = Bytes.new(header.size + buf.size)
|
||||||
|
header.copy_to(new_buf)
|
||||||
|
buf.copy_to(new_buf + header.size)
|
||||||
|
|
||||||
|
@socket.write(new_buf)
|
||||||
|
end
|
||||||
|
|
||||||
|
private def create_header(sequence : UInt16, time : UInt32) : Bytes
|
||||||
|
io = IO::Memory.new(12)
|
||||||
|
|
||||||
|
# Write the magic bytes required by Discord
|
||||||
|
io.write_byte(0x80_u8)
|
||||||
|
io.write_byte(0x78_u8)
|
||||||
|
|
||||||
|
# Write the actual information in the header
|
||||||
|
io.write_bytes(sequence, IO::ByteFormat::BigEndian)
|
||||||
|
io.write_bytes(time, IO::ByteFormat::BigEndian)
|
||||||
|
io.write_bytes(@ssrc.not_nil!, IO::ByteFormat::BigEndian)
|
||||||
|
|
||||||
|
io.to_slice
|
||||||
|
end
|
||||||
|
|
||||||
|
private def encrypt_audio(header : Bytes, buf : Bytes) : Bytes
|
||||||
|
raise "No secret key was set!" unless @secret_key
|
||||||
|
|
||||||
|
nonce = Bytes.new(24, 0_u8) # 24 null bytes
|
||||||
|
header.copy_to(nonce) # First 12 bytes of nonce is the header
|
||||||
|
|
||||||
|
# Sodium constants
|
||||||
|
zero_bytes = Sodium.crypto_secretbox_xsalsa20poly1305_zerobytes
|
||||||
|
box_zero_bytes = Sodium.crypto_secretbox_xsalsa20poly1305_boxzerobytes
|
||||||
|
|
||||||
|
# Prepend the buf with zero_bytes zero bytes
|
||||||
|
message = Bytes.new(buf.size + zero_bytes, 0_u8)
|
||||||
|
buf.copy_to(message + zero_bytes)
|
||||||
|
|
||||||
|
# Create a buffer for the ciphertext
|
||||||
|
c = Bytes.new(message.size)
|
||||||
|
|
||||||
|
# Encrypt
|
||||||
|
Sodium.crypto_secretbox_xsalsa20poly1305(c, message, message.bytesize, nonce, @secret_key.not_nil!)
|
||||||
|
|
||||||
|
# The resulting ciphertext buffer has box_zero_bytes zero bytes prepended;
|
||||||
|
# we don't want them in the result, so move the slice forward by that many
|
||||||
|
# bytes
|
||||||
|
c + box_zero_bytes
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Utility function that runs the given block and measures the time it takes,
|
||||||
|
# then sleeps the given time minus that time. This is useful for voice code
|
||||||
|
# because (in most cases) voice data should be sent to Discord at a rate of
|
||||||
|
# one frame every 20 ms, and if the processing and sending takes a certain
|
||||||
|
# amount of time, then noticeable choppiness can be heard.
|
||||||
|
def self.timed_run(total_time : Time::Span)
|
||||||
|
t1 = Time.now
|
||||||
|
yield
|
||||||
|
delta = Time.now - t1
|
||||||
|
|
||||||
|
sleep_time = {total_time - delta, Time::Span.zero}.max
|
||||||
|
sleep sleep_time
|
||||||
|
end
|
||||||
|
|
||||||
|
# Runs the given block every *time_span*. This method takes into account the
|
||||||
|
# execution time for the block to keep the intervals accurate.
|
||||||
|
#
|
||||||
|
# Note that if the block takes longer to execute than the given *time_span*,
|
||||||
|
# there will be no delay: the next iteration follows immediately, with no
|
||||||
|
# attempt to get in sync.
|
||||||
|
def self.every(time_span : Time::Span)
|
||||||
|
loop do
|
||||||
|
timed_run(time_span) { yield }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
require "http"
|
||||||
|
|
||||||
|
module Discord
|
||||||
|
# Internal wrapper around HTTP::WebSocket to decode the Discord-specific
|
||||||
|
# payload format used in the gateway and VWS.
|
||||||
|
class WebSocket
|
||||||
|
# :nodoc:
|
||||||
|
struct Packet
|
||||||
|
getter opcode, sequence, data, event_type
|
||||||
|
|
||||||
|
def initialize(@opcode : Int64?, @sequence : Int64?, @data : IO::Memory, @event_type : String?)
|
||||||
|
end
|
||||||
|
|
||||||
|
def inspect(io : IO)
|
||||||
|
io << "Discord::WebSocket::Packet(@opcode="
|
||||||
|
opcode.inspect(io)
|
||||||
|
io << " @sequence="
|
||||||
|
sequence.inspect(io)
|
||||||
|
io << " @data="
|
||||||
|
data.to_s.inspect(io)
|
||||||
|
io << " @event_type="
|
||||||
|
event_type.inspect(io)
|
||||||
|
io << ')'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(@host : String, @path : String, @port : Int32, @tls : Bool)
|
||||||
|
@websocket = HTTP::WebSocket.new(
|
||||||
|
host: @host,
|
||||||
|
path: @path,
|
||||||
|
port: @port,
|
||||||
|
tls: @tls
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def on_message(&handler : Packet ->)
|
||||||
|
@websocket.on_message do |message|
|
||||||
|
payload = parse_message(message)
|
||||||
|
handler.call(payload)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def on_close(&handler : String ->)
|
||||||
|
@websocket.on_close(&handler)
|
||||||
|
end
|
||||||
|
|
||||||
|
delegate run, close, send, to: @websocket
|
||||||
|
|
||||||
|
private def parse_message(message : String)
|
||||||
|
parser = JSON::PullParser.new(message)
|
||||||
|
|
||||||
|
opcode = nil
|
||||||
|
sequence = nil
|
||||||
|
event_type = nil
|
||||||
|
data = IO::Memory.new
|
||||||
|
|
||||||
|
parser.read_object do |key|
|
||||||
|
case key
|
||||||
|
when "op"
|
||||||
|
opcode = parser.read_int
|
||||||
|
when "d"
|
||||||
|
# Read the raw JSON into memory
|
||||||
|
JSON.build(data) do |builder|
|
||||||
|
parser.read_raw(builder)
|
||||||
|
end
|
||||||
|
when "s"
|
||||||
|
sequence = parser.read_int_or_null
|
||||||
|
when "t"
|
||||||
|
event_type = parser.read_string_or_null
|
||||||
|
else
|
||||||
|
# Unknown field
|
||||||
|
parser.skip
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Rewind to beginning of JSON
|
||||||
|
data.rewind
|
||||||
|
|
||||||
|
Packet.new(opcode, sequence, data, event_type)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,4 +1,16 @@
|
|||||||
|
require "discordcr"
|
||||||
|
|
||||||
|
token = "Bot NDIzNTkyMjczMTUxMDAwNTc2.DghCOQ.TITKUFX_WZDIh2j6kiprGTQ0Chw"
|
||||||
|
id = 423592273151000576_u64
|
||||||
|
prefix = '+'
|
||||||
|
|
||||||
|
|
||||||
|
client = Discord::Client.new(token: token, client_id: id)
|
||||||
|
|
||||||
|
client.on_message_create do |payload|
|
||||||
|
if payload.content.starts_with? prefix
|
||||||
|
client.create_message(payload.channel_id, "Pong!")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
client.run()
|
||||||
6
Crystal/First-Program/shard.lock
Normal file
6
Crystal/First-Program/shard.lock
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
version: 1.0
|
||||||
|
shards:
|
||||||
|
discordcr:
|
||||||
|
github: z64/discordcr
|
||||||
|
commit: 5c722de5c25a6020466e80bc10d7904a0e30e7d5
|
||||||
|
|
||||||
19
Crystal/First-Program/shard.yml
Normal file
19
Crystal/First-Program/shard.yml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
name: First-Program
|
||||||
|
version: 0.1.0
|
||||||
|
|
||||||
|
# authors:
|
||||||
|
# - name <email@example.com>
|
||||||
|
|
||||||
|
# description: |
|
||||||
|
# Short description of First-Program
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
discordcr:
|
||||||
|
github: z64/discordcr
|
||||||
|
branch: crystal-0.25
|
||||||
|
|
||||||
|
# development_dependencies:
|
||||||
|
# webmock:
|
||||||
|
# github: manastech/webmock.cr
|
||||||
|
|
||||||
|
license: MIT
|
||||||
Reference in New Issue
Block a user