Initial Commit
Initial commit of Code Base.
2
.gitignore
vendored
|
|
@ -414,4 +414,4 @@ FodyWeavers.xsd
|
|||
|
||||
# JetBrains Rider
|
||||
*.sln.iml
|
||||
|
||||
.idea/
|
||||
|
|
|
|||
22
Library/Singletons/GifManager.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
using Godot;
|
||||
|
||||
namespace PokePurple.Library.Singletons;
|
||||
|
||||
public class GifManager
|
||||
{
|
||||
private static GifManager? _instance;
|
||||
public static GifManager Instance => _instance ?? (_instance = new GifManager());
|
||||
|
||||
private Node _gifManager;
|
||||
|
||||
public GifManager()
|
||||
{
|
||||
_gifManager = ((SceneTree)Engine.GetMainLoop()).GetRoot().GetNode("GifManager");
|
||||
}
|
||||
|
||||
public AnimatedTexture AnimatedTextureFromBuffer(byte[] data, int max_frames = 0) => _gifManager.Call("animated_texture_from_buffer", data, max_frames).As<AnimatedTexture>();
|
||||
public AnimatedTexture AnimatedTextureFromFile(string file, int max_frames = 0) => _gifManager.Call("animated_texture_from_file", file, max_frames).As<AnimatedTexture>();
|
||||
|
||||
public SpriteFrames SpriteFramesFromBuffer(byte[] data, int max_frames = 0) => _gifManager.Call("sprite_frames_from_buffer", data, max_frames).As<SpriteFrames>();
|
||||
public SpriteFrames SpriteFramesFromFile(string file, int max_frames = 0) => _gifManager.Call("sprite_frames_from_file", file, max_frames).As<SpriteFrames>();
|
||||
}
|
||||
1
Library/Singletons/GifManager.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dphxqo51pj8i7
|
||||
12
Library/Singletons/Globals.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
using Godot;
|
||||
using System;
|
||||
using Godot.Sharp.Extras;
|
||||
|
||||
public partial class Globals : Node
|
||||
{
|
||||
[NodePath] private Node _twitchService;
|
||||
public override void _Ready()
|
||||
{
|
||||
this.OnReady();
|
||||
}
|
||||
}
|
||||
1
Library/Singletons/Globals.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://6nrfqrw53c1s
|
||||
47
Library/Singletons/globals.tscn
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
[gd_scene load_steps=12 format=3 uid="uid://b8whblp2hgrp"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://6nrfqrw53c1s" path="res://Library/Singletons/Globals.cs" id="1_gsl6h"]
|
||||
[ext_resource type="Script" uid="uid://i8st3lv0lidh" path="res://addons/twitcher/twitch_service.gd" id="1_ocehi"]
|
||||
[ext_resource type="Script" uid="uid://dcrliedgr6eol" path="res://addons/twitcher/lib/oOuch/crypto_key_provider.gd" id="2_gsl6h"]
|
||||
[ext_resource type="Script" uid="uid://00xbijwpi8xa" path="res://addons/twitcher/lib/oOuch/oauth_setting.gd" id="3_amvha"]
|
||||
[ext_resource type="Script" uid="uid://b3n3et8mebjcc" path="res://addons/twitcher/auth/twitch_oauth_scopes.gd" id="4_fcooh"]
|
||||
[ext_resource type="Resource" uid="uid://c4scwuk8q0r40" path="res://addons/twitcher/lib/oOuch/default_key_provider.tres" id="5_65rqk"]
|
||||
[ext_resource type="Script" uid="uid://b52xp7c23ucfk" path="res://addons/twitcher/lib/oOuch/oauth_token.gd" id="6_lppa0"]
|
||||
|
||||
[sub_resource type="Resource" id="Resource_54bgt"]
|
||||
script = ExtResource("2_gsl6h")
|
||||
encrpytion_secret_location = "user://encryption_key.cfg"
|
||||
|
||||
[sub_resource type="Resource" id="Resource_km1mv"]
|
||||
script = ExtResource("3_amvha")
|
||||
redirect_url = "http://localhost:7170"
|
||||
well_known_url = ""
|
||||
token_url = "https://id.twitch.tv/oauth2/token"
|
||||
authorization_url = "https://id.twitch.tv/oauth2/authorize"
|
||||
device_authorization_url = "https://id.twitch.tv/oauth2/device"
|
||||
cache_file = "user://auth.conf"
|
||||
client_id = "xqh437vs83im368db89iwflm130ufb"
|
||||
authorization_flow = 0
|
||||
_encryption_key_provider = SubResource("Resource_54bgt")
|
||||
client_secret = "D8bwpU1vz2zu6P9l0D3ktvctJMCuFspuG//RPTsyP8w="
|
||||
|
||||
[sub_resource type="Resource" id="Resource_to4h6"]
|
||||
script = ExtResource("4_fcooh")
|
||||
used_scopes = Array[StringName]([&"moderation:read", &"user:bot", &"user:read:chat", &"user:write:chat", &"chat:edit", &"chat:read"])
|
||||
metadata/_custom_type_script = "uid://b3n3et8mebjcc"
|
||||
|
||||
[sub_resource type="Resource" id="Resource_3e7ys"]
|
||||
script = ExtResource("6_lppa0")
|
||||
_crypto_key_provider = ExtResource("5_65rqk")
|
||||
_identifier = "EditorToken"
|
||||
_cache_path = "user://auth.conf"
|
||||
|
||||
[node name="Globals" type="Node"]
|
||||
script = ExtResource("1_gsl6h")
|
||||
|
||||
[node name="TwitchService" type="Node" parent="."]
|
||||
script = ExtResource("1_ocehi")
|
||||
oauth_setting = SubResource("Resource_km1mv")
|
||||
scopes = SubResource("Resource_to4h6")
|
||||
token = SubResource("Resource_3e7ys")
|
||||
metadata/_custom_type_script = "uid://i8st3lv0lidh"
|
||||
11
PokePurple.csproj
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<Project Sdk="Godot.NET.Sdk/4.4.1">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="GodotSharpExtras" Version="0.5.0" />
|
||||
<PackageReference Include="PokeApiNet" Version="4.0.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
19
PokePurple.sln
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 2012
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PokePurple", "PokePurple.csproj", "{5C940B04-31D7-4268-9B7E-63D5C488E573}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
ExportDebug|Any CPU = ExportDebug|Any CPU
|
||||
ExportRelease|Any CPU = ExportRelease|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{5C940B04-31D7-4268-9B7E-63D5C488E573}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5C940B04-31D7-4268-9B7E-63D5C488E573}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5C940B04-31D7-4268-9B7E-63D5C488E573}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU
|
||||
{5C940B04-31D7-4268-9B7E-63D5C488E573}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU
|
||||
{5C940B04-31D7-4268-9B7E-63D5C488E573}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU
|
||||
{5C940B04-31D7-4268-9B7E-63D5C488E573}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
21
addons/godotgif/LICENSE.txt
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2023 B0TLANNER Games
|
||||
|
||||
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.
|
||||
139
addons/godotgif/README.md
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
# Godot GIF
|
||||
<br>
|
||||
|
||||
<p align="center">
|
||||
<img src="./docs-images/logo.gif" alt="Logo" width="128" height="128" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/BOTLANNER/godot-gif/actions/workflows/build_releases.yml"><img alt="GitHub Build" src="https://github.com/BOTLANNER/godot-gif/actions/workflows/build_releases.yml/badge.svg" height="20"/></a>
|
||||
<a href="https://github.com/BOTLANNER/godot-gif/blob/develop/LICENSE.txt"><img alt="MIT License" src="https://img.shields.io/github/license/BOTLANNER/godot-gif" height="20"/></a>
|
||||
</p>
|
||||
|
||||
|
||||
## Description
|
||||
GDExtension for Godot 4+ to load GIF files as [AnimatedTexture](https://docs.godotengine.org/en/stable/classes/class_animatedtexture.html) and/or [SpriteFrames](https://docs.godotengine.org/en/stable/classes/class_spriteframes.html).
|
||||
|
||||
NOTE: ~~**AnimatedTexture**~~ has been marked as deprecated according to development docs and could be removed in a future version of Godot.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
### Editor
|
||||
|
||||
Gif files can be imported at edit time as one of the supported types via Import options.
|
||||
|
||||
<details open>
|
||||
<summary>Editor Imports Options</summary>
|
||||
|
||||

|
||||
</details>
|
||||
|
||||
See the [Editor Imports](./demo/editor_imports_example.tscn) example scene.
|
||||
<details open>
|
||||
<summary>Editor Imports Example</summary>
|
||||
|
||||

|
||||
</details>
|
||||
|
||||
<hr/>
|
||||
|
||||
### Runtime
|
||||
|
||||
Gif files can be loaded at runtime as one of the supported types via the `GifManager` singleton.
|
||||
|
||||
`GifManager` exposes the following methods for loading gifs either from file or from bytes directly:
|
||||

|
||||
|
||||
e.g. to load from file
|
||||
```py
|
||||
get_node("AnimFromRuntimeFile").texture = GifManager.animated_texture_from_file("res://examples/file/optic.gif")
|
||||
|
||||
get_node("AnimatedSprite2RuntimeFile").sprite_frames = GifManager.sprite_frames_from_file("res://examples/file/optic.gif")
|
||||
```
|
||||
|
||||
See the [Runtime Imports](./demo/main.tscn) example scene.
|
||||
<details open>
|
||||
<summary>Runtime Imports Example</summary>
|
||||
|
||||

|
||||
</details>
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
Download the `gdextension` artifact from the [latest successful build](https://github.com/BOTLANNER/godot-gif/actions/workflows/build_releases.yml). (It should be right at the bottom of the **Summary**)
|
||||

|
||||
|
||||
Extract the contents to your Godot project directory.
|
||||
|
||||
You should have an `addons` directory at the root with the following structure:
|
||||
```bash
|
||||
└───addons
|
||||
└───godotgif
|
||||
│ godotgif.gdextension
|
||||
│ LICENSE.txt
|
||||
│ README.md
|
||||
│
|
||||
└───bin
|
||||
│ godotgif.windows.template_debug.x86_32.dll
|
||||
│ godotgif.windows.template_debug.x86_64.dll
|
||||
│ godotgif.windows.template_release.x86_32.dll
|
||||
│ godotgif.windows.template_release.x86_64.dll
|
||||
│ libgodotgif.android.template_debug.arm64.so
|
||||
│ libgodotgif.android.template_release.arm64.so
|
||||
│ libgodotgif.linux.template_debug.x86_32.so
|
||||
│ libgodotgif.linux.template_debug.x86_64.so
|
||||
│ libgodotgif.linux.template_release.x86_32.so
|
||||
│ libgodotgif.linux.template_release.x86_64.so
|
||||
│
|
||||
├───godotgif.macos.template_debug.framework
|
||||
│ libgodotgif.macos.template_debug
|
||||
│
|
||||
└───godotgif.macos.template_release.framework
|
||||
libgodotgif.macos.template_release
|
||||
```
|
||||
|
||||
Open your project. Any exisitng gifs should auto-import. New gifs in the project directory will automatically import as `SpriteFrames`. To convert them into `AnimatedTexture`, update the [import settings](#editor).
|
||||
|
||||
The `GifManager` class should also now be available for access within GDScript.
|
||||
|
||||
## Contributing
|
||||
|
||||
### Setup
|
||||
|
||||
Ensure **SCons** is setup. Refer to [Introduction to the buildsystem](https://docs.godotengine.org/en/stable/contributing/development/compiling/introduction_to_the_buildsystem.html)
|
||||
|
||||
* If using a different version of Godot, be sure to dump the bindings e.g.
|
||||
```sh
|
||||
godot --dump-extension-api extension_api.json
|
||||
```
|
||||
* Compile with
|
||||
```sh
|
||||
scons platform=<platform> custom_api_file=extension_api.json
|
||||
```
|
||||
|
||||
### Debugging
|
||||
|
||||
This repository is configured for use with [VSCode](https://code.visualstudio.com/)
|
||||
|
||||
[Launch configurations](./.vscode/launch.json) have been setup for both debugging in editor and in runtime provided certain **VSCode** extensions are present and environment variables are defined.
|
||||
|
||||
The following environment variables are required:
|
||||
|
||||
1. `GODOT_PATH` - The directory in which Godot is installed
|
||||
1. `GODOT_EXECUTABLE` - The executable name of the Godot installation
|
||||
|
||||
### More Details
|
||||
Refer to [GDExtension C++ example](https://docs.godotengine.org/en/stable/tutorials/scripting/gdextension/gdextension_cpp_example.html)
|
||||
|
||||
|
||||
## License
|
||||
|
||||
Unless otherwise specified, the extension is released under the
|
||||
[MIT license](LICENSE.txt).
|
||||
|
||||
See the full list of third-party libraries with their licenses used by this
|
||||
extension at [src/thirdparty/README.md](src/thirdparty/README.md).
|
||||
|
||||
This implementation heavily borrowed inspiration from the [gif module](https://github.com/goostengine/goost/tree/gd3/modules/gif) for [Goost](https://github.com/goostengine/goost) that is currently only based on Godot 3
|
||||
BIN
addons/godotgif/bin/godotgif.windows.template_debug.x86_32.dll
Normal file
BIN
addons/godotgif/bin/godotgif.windows.template_debug.x86_64.dll
Normal file
BIN
addons/godotgif/bin/godotgif.windows.template_release.x86_32.dll
Normal file
BIN
addons/godotgif/bin/godotgif.windows.template_release.x86_64.dll
Normal file
BIN
addons/godotgif/bin/libgodotgif.android.template_debug.arm64.so
Normal file
BIN
addons/godotgif/bin/libgodotgif.linux.template_debug.x86_32.so
Normal file
BIN
addons/godotgif/bin/libgodotgif.linux.template_debug.x86_64.so
Normal file
BIN
addons/godotgif/bin/libgodotgif.linux.template_release.x86_32.so
Normal file
BIN
addons/godotgif/bin/libgodotgif.linux.template_release.x86_64.so
Normal file
23
addons/godotgif/godotgif.gdextension
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
[configuration]
|
||||
|
||||
entry_symbol = "godot_gif_library_init"
|
||||
compatibility_minimum = "4.4"
|
||||
|
||||
[libraries]
|
||||
|
||||
macos.debug = "bin/godotgif.macos.template_debug.framework/libgodotgif.macos.template_debug"
|
||||
macos.release = "bin/godotgif.macos.template_release.framework/libgodotgif.macos.template_release"
|
||||
windows.debug.x86_32 = "bin/godotgif.windows.template_debug.x86_32.dll"
|
||||
windows.release.x86_32 = "bin/godotgif.windows.template_release.x86_32.dll"
|
||||
windows.debug.x86_64 = "bin/godotgif.windows.template_debug.x86_64.dll"
|
||||
windows.release.x86_64 = "bin/godotgif.windows.template_release.x86_64.dll"
|
||||
linux.debug.x86_64 = "bin/libgodotgif.linux.template_debug.x86_64.so"
|
||||
linux.release.x86_64 = "bin/libgodotgif.linux.template_release.x86_64.so"
|
||||
linux.debug.arm64 = "bin/libgodotgif.linux.template_debug.arm64.so"
|
||||
linux.release.arm64 = "bin/libgodotgif.linux.template_release.arm64.so"
|
||||
linux.debug.rv64 = "bin/libgodotgif.linux.template_debug.rv64.so"
|
||||
linux.release.rv64 = "bin/libgodotgif.linux.template_release.rv64.so"
|
||||
android.debug.x86_64 = "bin/libgodotgif.android.template_debug.x86_64.so"
|
||||
android.release.x86_64 = "bin/libgodotgif.android.template_release.x86_64.so"
|
||||
android.debug.arm64 = "bin/libgodotgif.android.template_debug.arm64.so"
|
||||
android.release.arm64 = "bin/libgodotgif.android.template_release.arm64.so"
|
||||
1
addons/godotgif/godotgif.gdextension.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://xpotkvhtirqd
|
||||
1
addons/twitcher/COPYRIGHT.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
Copyright Kemomi 2024 - 2025. All rights reserved.
|
||||
21
addons/twitcher/LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2024 Kani
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
1
addons/twitcher/assets/api-icon.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><circle cx="5.878" cy="8" r="4.725" style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#e0e0e0;stroke-width:1.06125;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none"/><circle cx="14.06" cy="1.677" r="1.318" style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#e0e0e0;fill-opacity:1;stroke:none;stroke-width:1.20539;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none"/><circle cx="14.06" cy="8" r="1.318" style="font-variation-settings:normal;vector-effect:none;fill:#e0e0e0;fill-opacity:1;stroke:none;stroke-width:1.20539;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none"/><circle cx="14.06" cy="14.323" r="1.318" style="font-variation-settings:normal;vector-effect:none;fill:#e0e0e0;fill-opacity:1;stroke:none;stroke-width:1.20539;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none"/><path d="M7.374 3.724 9.827 1.6h4.268" style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#e0e0e0;stroke-width:1.06125;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000;stop-opacity:1"/><path d="m7.353 12.348 2.474 2.105h4.268" style="font-variation-settings:normal;vector-effect:none;fill:none;fill-opacity:1;stroke:#e0e0e0;stroke-width:1.06129;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000"/><path d="M10.258 8.02 14.122 8" style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#e0e0e0;stroke-width:1.06125;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none"/></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
37
addons/twitcher/assets/api-icon.svg.import
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://sp45xp1nleuk"
|
||||
path="res://.godot/imported/api-icon.svg-16588d9bd32b7709dca1da03b9d3c51f.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/twitcher/assets/api-icon.svg"
|
||||
dest_files=["res://.godot/imported/api-icon.svg-16588d9bd32b7709dca1da03b9d3c51f.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
1
addons/twitcher/assets/auth-icon.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="none" stroke="#e0e0e0" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="1.5" d="M1.7 2h13v9.1H7.5l-2.9 2.6v-2.6H1.8z" style="stroke-width:1;stroke-dasharray:none"/><path d="M3.305 4.018v4.575a.356.356 45 0 0 .356.356h1.573a.56.56 147.482 0 0 .506-.323l.76-1.632h2.973l.468 1.322h1.268l.55-1.322h1.008a.356.356 135 0 0 .356-.356V5.96a.356.356 45 0 0-.356-.356H6.5l-.759-1.62a.56.56 32.441 0 0-.507-.322H3.66a.356.356 135 0 0-.356.356m1.113.779h.399a.45.45 34.142 0 1 .42.284l.496 1.246v.002l.002.004h-.002l-.531 1.332a.315.315 145.858 0 1-.292.199H4.51a.417.417 45 0 1-.417-.418V5.121a.324.324 135 0 1 .324-.324" style="fill:#e0e0e0"/></svg>
|
||||
|
After Width: | Height: | Size: 747 B |
37
addons/twitcher/assets/auth-icon.svg.import
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://1kwo4knd8h05"
|
||||
path="res://.godot/imported/auth-icon.svg-8c3337a2de3e7408dd8b0185710eb409.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/twitcher/assets/auth-icon.svg"
|
||||
dest_files=["res://.godot/imported/auth-icon.svg-8c3337a2de3e7408dd8b0185710eb409.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
1
addons/twitcher/assets/chat-icon.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="none" stroke="#e0e0e0" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="1.5" d="M1.7 2h13v9.1H7.5l-2.9 2.6v-2.6H1.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 232 B |
37
addons/twitcher/assets/chat-icon.svg.import
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://txhld57vfpmo"
|
||||
path="res://.godot/imported/chat-icon.svg-e1164fbfcc337348789c82b706287c92.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/twitcher/assets/chat-icon.svg"
|
||||
dest_files=["res://.godot/imported/chat-icon.svg-e1164fbfcc337348789c82b706287c92.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
1
addons/twitcher/assets/command-icon.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="none" stroke="#e0e0e0" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="1.5" d="m8 1.633 2 2.553-2 6.188-2-6.188Z" style="fill:#e0e0e0;fill-opacity:1"/><circle cx="8" cy="13.494" r="1.57" style="fill:#e0e0e0;fill-opacity:1"/></svg>
|
||||
|
After Width: | Height: | Size: 338 B |
37
addons/twitcher/assets/command-icon.svg.import
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dr6bv6l3g4as3"
|
||||
path="res://.godot/imported/command-icon.svg-29aa6e8d352722ad63fdf993d5c2472a.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/twitcher/assets/command-icon.svg"
|
||||
dest_files=["res://.godot/imported/command-icon.svg-29aa6e8d352722ad63fdf993d5c2472a.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
82
addons/twitcher/assets/error-page.txt
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Twitcher - Login Failed</title>
|
||||
<script>setTimeout(() => window.close(), 5000);</script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background: linear-gradient(135deg, #2d2d2d, #1e1e1e);
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.error-container {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 30px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(10px);
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 60px;
|
||||
margin-bottom: 15px;
|
||||
color: #e63946;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.error-description {
|
||||
font-size: 18px;
|
||||
margin-top: 10px;
|
||||
text-align: justify;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
margin-top: 20px;
|
||||
padding: 12px 25px;
|
||||
background-color: #e63946;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
border-radius: 5px;
|
||||
transition: background 0.3s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: #d62839;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="error-container">
|
||||
<div class="error-message">Login Failed</div>
|
||||
<div class="error-description">Login attempt was unsuccessful. Page should automatically close when it doesn't happen close it manually.</div>
|
||||
<a onclick="window.close()" class="button">Close Page</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
1
addons/twitcher/assets/event-icon.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M8.498 2.047c-.117-.015-4.11-.5-4.478 4.18A8 8 0 0 0 4 6.54c-.104 2.13.165 5.366-2.07 6.457a.6.6 0 0 0-.145.037q-.006.004-.008.008 0 .004.004.008c.176.146 6.066.061 6.262.058 1.508.023 5.666.065 6.115-.043a.1.1 0 0 0 .037-.015q.005-.004.004-.008c-.004-.013-2.036-.01-2.209-5.447h0" style="opacity:1;fill:none;stroke:#e0e0e0;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stop-color:#000;stop-opacity:1"/><path d="M9.873 13.742a1.928 2.01 0 0 1-.964 1.74 1.928 2.01 0 0 1-1.928 0 1.928 2.01 0 0 1-.964-1.74h1.928Z" style="font-variation-settings:normal;opacity:1;fill:#e0e0e0;fill-opacity:1;stroke:#e0e0e0;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"/><circle cx="11.411" cy="4.089" r="2.22" style="font-variation-settings:normal;opacity:1;fill:#e0e0e0;fill-opacity:1;stroke:#e0e0e0;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
37
addons/twitcher/assets/event-icon.svg.import
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://co7dy71iroidu"
|
||||
path="res://.godot/imported/event-icon.svg-ae206972b8adb971cf3c156d8f980130.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/twitcher/assets/event-icon.svg"
|
||||
dest_files=["res://.godot/imported/event-icon.svg-ae206972b8adb971cf3c156d8f980130.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
1
addons/twitcher/assets/eventsub-icon.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M5.484 2.484v9.594M8 2.484v9.594M10.516 2.484v9.594" style="fill:#e0e0e0;fill-opacity:1;stroke:#e0e0e0;stroke-width:2;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"/><path d="m2.818 10.352.022 4.22h10.342v-3.955" style="fill:none;fill-opacity:1;stroke:#e0e0e0;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"/><path d="m.972 3.57 2.245 2.883L.972 9.337M13.18 3.393l2.245 2.883L13.18 9.16" style="fill:#e0e0e0;fill-opacity:1;stroke:none;stroke-width:1.48044;stroke-linecap:butt;stroke-dasharray:none;stroke-opacity:1"/></svg>
|
||||
|
After Width: | Height: | Size: 634 B |
37
addons/twitcher/assets/eventsub-icon.svg.import
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dykujenp3l608"
|
||||
path="res://.godot/imported/eventsub-icon.svg-ce0c1dc80296ea912c84ec427215a2e3.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/twitcher/assets/eventsub-icon.svg"
|
||||
dest_files=["res://.godot/imported/eventsub-icon.svg-ce0c1dc80296ea912c84ec427215a2e3.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
1
addons/twitcher/assets/ext-link.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="none" stroke="#e0e0e0" stroke-width="1.411" d="M6.04 1.654H1.36v12.692h13.28V10.24"/><path fill="#e0e0e0" d="M15.346.948v6.223l-2.14 2.14V4.643L8.734 9.116 7.178 7.56l4.473-4.473H6.983L9.123.948z"/></svg>
|
||||
|
After Width: | Height: | Size: 279 B |
37
addons/twitcher/assets/ext-link.svg.import
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bwgk2bv7wbbo7"
|
||||
path="res://.godot/imported/ext-link.svg-e60b226c6ba08585f6582dc226fddebb.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/twitcher/assets/ext-link.svg"
|
||||
dest_files=["res://.godot/imported/ext-link.svg-e60b226c6ba08585f6582dc226fddebb.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
9
addons/twitcher/assets/fallback_texture.tres
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
[gd_resource type="GradientTexture1D" load_steps=2 format=3 uid="uid://g1dbcjksbotw"]
|
||||
|
||||
[sub_resource type="Gradient" id="Gradient_dsf6b"]
|
||||
offsets = PackedFloat32Array(0)
|
||||
colors = PackedColorArray(0.921569, 0.227451, 0.988235, 1)
|
||||
|
||||
[resource]
|
||||
gradient = SubResource("Gradient_dsf6b")
|
||||
width = 1
|
||||
BIN
addons/twitcher/assets/favicon.ico
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
13
addons/twitcher/assets/icon_search.tres
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
[gd_resource type="ImageTexture" load_steps=2 format=3 uid="uid://1e6nrtqsuc6"]
|
||||
|
||||
[sub_resource type="Image" id="Image_mutbh"]
|
||||
data = {
|
||||
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 225, 225, 225, 68, 224, 224, 224, 184, 224, 224, 224, 240, 224, 224, 224, 232, 224, 224, 224, 186, 227, 227, 227, 62, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 129, 224, 224, 224, 254, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 122, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 225, 225, 225, 68, 224, 224, 224, 254, 224, 224, 224, 254, 224, 224, 224, 123, 224, 224, 224, 32, 224, 224, 224, 33, 225, 225, 225, 125, 224, 224, 224, 254, 224, 224, 224, 254, 226, 226, 226, 69, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 184, 224, 224, 224, 255, 224, 224, 224, 123, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 225, 225, 225, 125, 224, 224, 224, 255, 225, 225, 225, 174, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 240, 224, 224, 224, 255, 231, 231, 231, 31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 226, 226, 226, 35, 224, 224, 224, 255, 224, 224, 224, 233, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 232, 224, 224, 224, 255, 224, 224, 224, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 228, 228, 228, 37, 224, 224, 224, 255, 224, 224, 224, 228, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 186, 224, 224, 224, 255, 224, 224, 224, 123, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 130, 224, 224, 224, 255, 224, 224, 224, 173, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 227, 227, 227, 62, 224, 224, 224, 255, 224, 224, 224, 254, 225, 225, 225, 126, 225, 225, 225, 34, 227, 227, 227, 36, 224, 224, 224, 131, 224, 224, 224, 255, 224, 224, 224, 255, 226, 226, 226, 77, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 122, 224, 224, 224, 254, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 226, 226, 226, 69, 225, 225, 225, 174, 224, 224, 224, 233, 224, 224, 224, 228, 224, 224, 224, 173, 226, 226, 226, 77, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 227, 225, 225, 225, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 231, 231, 231, 21, 225, 225, 225, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
|
||||
"format": "RGBA8",
|
||||
"height": 16,
|
||||
"mipmaps": false,
|
||||
"width": 16
|
||||
}
|
||||
|
||||
[resource]
|
||||
image = SubResource("Image_mutbh")
|
||||
4
addons/twitcher/assets/info_label_settings.tres
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
[gd_resource type="LabelSettings" format=3 uid="uid://d12dapnv7b00n"]
|
||||
|
||||
[resource]
|
||||
font_color = Color(0.400671, 0.976237, 1, 1)
|
||||
1
addons/twitcher/assets/media-loader-icon.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M3.648 1.438h11.003v8.884H3.648z" style="fill:none;stroke:#e0e0e0;stroke-width:1;stroke-linejoin:round;stroke-dasharray:none"/><path d="M3.857 9.927 7.408 5.02s2.28 3.284 2.28 3.185 1.88-1.74 1.88-1.74l2.786 3.482z" style="fill:#e0e0e0;fill-opacity:1;stroke:#e0e0e0;stroke-width:.839927;stroke-linejoin:round;-inkscape-stroke:none"/><circle cx="10.679" cy="3.746" r=".807" style="fill:#e0e0e0;fill-opacity:1;stroke:#e0e0e0;stroke-width:.890358;stroke-linejoin:round;-inkscape-stroke:none"/><path d="m1.145 10.558 2.658 4.157 2.858-4.157" style="fill:#73f280;fill-opacity:1;stroke:none;stroke-width:1.35663;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"/></svg>
|
||||
|
After Width: | Height: | Size: 744 B |
37
addons/twitcher/assets/media-loader-icon.svg.import
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://drsj3w203jihf"
|
||||
path="res://.godot/imported/media-loader-icon.svg-7569e70d457bf77b040db83e53346e05.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/twitcher/assets/media-loader-icon.svg"
|
||||
dest_files=["res://.godot/imported/media-loader-icon.svg-7569e70d457bf77b040db83e53346e05.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
BIN
addons/twitcher/assets/no_profile.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
35
addons/twitcher/assets/no_profile.png.import
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://6nflfslr4a52"
|
||||
path.s3tc="res://.godot/imported/no_profile.png-c0302c23dfe26865f13493d5d6d52fb6.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/twitcher/assets/no_profile.png"
|
||||
dest_files=["res://.godot/imported/no_profile.png-c0302c23dfe26865f13493d5d6d52fb6.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=true
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=0
|
||||
1
addons/twitcher/assets/service-icon.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="m11.932 2.744-1.089-.199-.983 1.416a4 4 0 0 0-.97.225l-1.635-.778-.845.713.474 1.693q-.266.417-.426.89l-1.623.768.016 1.104 1.62.707q.15.436.417.854l.021.032-.426 1.733.868.685 1.586-.816q.476.166.971.21l1.047 1.408 1.082-.233.385-1.771c.28-.174.541-.383.774-.626l1.72.088.51-.982-1.142-1.454q.051-.495-.015-.973l1.15-1.297-.453-1.01-1.845-.012a4 4 0 0 0-.736-.58Zm-.955 2.078a3.22 3.22 0 0 1 2.454 3.845c-.314 1.42-1.385 2.267-2.587 2.48-1.201.213-2.5-.216-3.281-1.441s-.624-2.583.075-3.583 1.919-1.615 3.339-1.301" style="fill:#e0e0e0;fill-opacity:1;stroke-width:.801266;stroke-linejoin:round"/><path d="M1.46 5.184h2.615" style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#e0e0e0;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none"/><path d="M1.46 10.816h2.615" style="font-variation-settings:normal;vector-effect:none;fill:none;fill-opacity:1;stroke:#e0e0e0;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none"/><path d="M.773 8h2.554" style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#e0e0e0;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none"/></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
37
addons/twitcher/assets/service-icon.svg.import
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cvvl6migokgdw"
|
||||
path="res://.godot/imported/service-icon.svg-e43d86792dba5abf158c9a90e8467717.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/twitcher/assets/service-icon.svg"
|
||||
dest_files=["res://.godot/imported/service-icon.svg-e43d86792dba5abf158c9a90e8467717.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
79
addons/twitcher/assets/success-page.txt
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Twitcher - Login</title>
|
||||
<script>setTimeout(() => window.close(), 5000);</script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background: linear-gradient(135deg, #22c55e, #16a34a);
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
color: #133620;
|
||||
}
|
||||
|
||||
.success-container {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 30px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(10px);
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
font-size: 60px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.success-description {
|
||||
font-size: 18px;
|
||||
margin-top: 10px;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
margin-top: 20px;
|
||||
padding: 12px 25px;
|
||||
background-color: white;
|
||||
color: #15803d;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
border-radius: 5px;
|
||||
transition: background 0.3s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: #d1fae5;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="success-container">
|
||||
<div class="success-message">Login Success</div>
|
||||
<div class="success-description">Page should automatically close when it doesn't happen close it manually.</div>
|
||||
<a onclick="window.close()" class="button">Close Page</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
9
addons/twitcher/assets/title_label_settings.tres
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
[gd_resource type="LabelSettings" load_steps=2 format=3 uid="uid://bnsxy6gcm8q11"]
|
||||
|
||||
[sub_resource type="SystemFont" id="SystemFont_rtf3j"]
|
||||
font_weight = 800
|
||||
force_autohinter = true
|
||||
|
||||
[resource]
|
||||
font = SubResource("SystemFont_rtf3j")
|
||||
font_size = 18
|
||||
9
addons/twitcher/assets/transparent.tres
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
[gd_resource type="GradientTexture1D" load_steps=2 format=3 uid="uid://bdhuy21ldt2vv"]
|
||||
|
||||
[sub_resource type="Gradient" id="Gradient_y3p12"]
|
||||
offsets = PackedFloat32Array(1)
|
||||
colors = PackedColorArray(1, 1, 1, 0)
|
||||
|
||||
[resource]
|
||||
gradient = SubResource("Gradient_y3p12")
|
||||
width = 1
|
||||
1
addons/twitcher/assets/twitcher-icon.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="none" stroke="#e0e0e0" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="1.5" d="M1.7 2h13v9.1H7.5l-2.9 2.6v-2.6H1.8z" style="stroke-width:1;stroke-dasharray:none"/><path d="M4.508 4.787h2.386v3.558H4.508z" style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#e0e0e0;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none"/><path d="M9.994 4.787h2.386v3.558H9.994z" style="font-variation-settings:normal;vector-effect:none;fill:#e0e0e0;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none"/></svg>
|
||||
|
After Width: | Height: | Size: 885 B |
37
addons/twitcher/assets/twitcher-icon.svg.import
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://ctljyyw6gikq0"
|
||||
path="res://.godot/imported/twitcher-icon.svg-eae8d458f370f3edcefb2f6360cb1ecc.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/twitcher/assets/twitcher-icon.svg"
|
||||
dest_files=["res://.godot/imported/twitcher-icon.svg-eae8d458f370f3edcefb2f6360cb1ecc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
4
addons/twitcher/assets/warning_label_settings.tres
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
[gd_resource type="LabelSettings" format=3 uid="uid://cng881nsuud80"]
|
||||
|
||||
[resource]
|
||||
font_color = Color(1, 0.870588, 0.4, 1)
|
||||
8
addons/twitcher/auth/preset_game_scopes.tres
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
[gd_resource type="Resource" script_class="TwitchOAuthScopes" load_steps=2 format=3 uid="uid://3dm6ts8hwlys"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://b3n3et8mebjcc" path="res://addons/twitcher/auth/twitch_oauth_scopes.gd" id="1_bpjq8"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_bpjq8")
|
||||
used_scopes = Array[StringName]([&"user:read:chat", &"user:write:chat", &"user:bot"])
|
||||
metadata/_custom_type_script = "uid://b3n3et8mebjcc"
|
||||
8
addons/twitcher/auth/preset_overlay_scopes.tres
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
[gd_resource type="Resource" script_class="TwitchOAuthScopes" load_steps=2 format=3 uid="uid://fcmfkstye4bq"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://b3n3et8mebjcc" path="res://addons/twitcher/auth/twitch_oauth_scopes.gd" id="1_2b4sa"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_2b4sa")
|
||||
used_scopes = Array[StringName]([&"user:read:chat", &"user:write:chat", &"user:bot"])
|
||||
metadata/_custom_type_script = "uid://b3n3et8mebjcc"
|
||||
135
addons/twitcher/auth/twitch_auth.gd
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
@icon("res://addons/twitcher/assets/auth-icon.svg")
|
||||
@tool
|
||||
extends Twitcher
|
||||
|
||||
## Delegate class for the oOuch Library.
|
||||
class_name TwitchAuth
|
||||
|
||||
const HttpUtil = preload("res://addons/twitcher/lib/http/http_util.gd")
|
||||
|
||||
static var _log: TwitchLogger = TwitchLogger.new("TwitchAuth")
|
||||
|
||||
## The requested devicecode to show to the user for authorization
|
||||
signal device_code_requested(device_code: OAuth.OAuthDeviceCodeResponse);
|
||||
|
||||
## Where and how to authorize.
|
||||
@export var oauth_setting: OAuthSetting:
|
||||
set(val):
|
||||
oauth_setting = val
|
||||
if auth != null: auth.oauth_setting = oauth_setting
|
||||
if token_handler != null: token_handler.oauth_setting = oauth_setting
|
||||
update_configuration_warnings()
|
||||
## Shows the what to authorize page of twitch again. (for example you need to relogin with a different account aka bot account)
|
||||
@export var force_verify: bool
|
||||
## Where should the tokens be saved into
|
||||
@export var token: OAuthToken:
|
||||
set(val):
|
||||
token = val
|
||||
if token_handler != null: token_handler.token = token
|
||||
update_configuration_warnings()
|
||||
## Scopes for the token that should be requested
|
||||
@export var scopes: OAuthScopes:
|
||||
set(val):
|
||||
scopes = val
|
||||
if auth != null: auth.scopes = scopes
|
||||
update_configuration_warnings()
|
||||
|
||||
## Takes care to authorize the user
|
||||
@onready var auth: OAuth
|
||||
## Takes care to fetch and refresh oauth tokens
|
||||
@onready var token_handler: TwitchTokenHandler
|
||||
|
||||
|
||||
var is_authenticated: bool:
|
||||
get(): return auth.is_authenticated()
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
child_entered_tree.connect(_on_enter_child)
|
||||
# There could be better locations but this ensures that its there when an
|
||||
# auth is needed.
|
||||
var http_logger = TwitchLogger.new("Http")
|
||||
HttpUtil.set_logger(http_logger.e, http_logger.i, http_logger.d)
|
||||
|
||||
|
||||
func _on_enter_child(node: Node) -> void:
|
||||
if node is OAuth: auth = node
|
||||
if node is TwitchTokenHandler: token_handler = node
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
OAuth.set_logger(_log.e, _log.i, _log.d);
|
||||
if oauth_setting == null: oauth_setting = create_default_oauth_setting()
|
||||
_ensure_children()
|
||||
|
||||
|
||||
func _ensure_children() -> void:
|
||||
if token_handler == null:
|
||||
token_handler = TwitchTokenHandler.new()
|
||||
token_handler.name = "TokenHandler"
|
||||
|
||||
if auth == null:
|
||||
auth = OAuth.new()
|
||||
auth.name = "OAuth"
|
||||
|
||||
_sync_childs()
|
||||
|
||||
if not auth.is_inside_tree():
|
||||
add_child(auth)
|
||||
auth.owner = owner
|
||||
if not token_handler.is_inside_tree():
|
||||
add_child(token_handler)
|
||||
token_handler.owner = owner
|
||||
|
||||
|
||||
func _sync_childs() -> void:
|
||||
token_handler.oauth_setting = oauth_setting
|
||||
token_handler.token = token
|
||||
auth.token_handler = token_handler
|
||||
auth.oauth_setting = oauth_setting
|
||||
auth.scopes = scopes
|
||||
auth.force_verify = &"true" if force_verify else &"false"
|
||||
|
||||
|
||||
func authorize() -> bool:
|
||||
_sync_childs()
|
||||
if await auth.login():
|
||||
token_handler.process_mode = Node.PROCESS_MODE_INHERIT
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func refresh_token() -> void:
|
||||
auth.refresh_token()
|
||||
|
||||
|
||||
static func create_default_oauth_setting() -> OAuthSetting:
|
||||
var oauth_setting = OAuthSetting.new()
|
||||
oauth_setting.authorization_flow = OAuth.AuthorizationFlow.AUTHORIZATION_CODE_FLOW
|
||||
oauth_setting.device_authorization_url = "https://id.twitch.tv/oauth2/device"
|
||||
oauth_setting.token_url = "https://id.twitch.tv/oauth2/token"
|
||||
oauth_setting.authorization_url = "https://id.twitch.tv/oauth2/authorize"
|
||||
oauth_setting.cache_file = "user://auth.conf"
|
||||
oauth_setting.redirect_url = "http://localhost:7170"
|
||||
return oauth_setting
|
||||
|
||||
|
||||
## Checks if the correctly setup
|
||||
func is_configured() -> bool:
|
||||
return _get_configuration_warnings().is_empty()
|
||||
|
||||
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var result: PackedStringArray = []
|
||||
if oauth_setting == null:
|
||||
result.append("OAuthSetting missing")
|
||||
else:
|
||||
var oauth_setting_problems : PackedStringArray = oauth_setting.get_valididation_problems()
|
||||
if not oauth_setting_problems.is_empty():
|
||||
result.append("OAuthSetting is invalid")
|
||||
result.append_array(oauth_setting_problems)
|
||||
if scopes == null:
|
||||
result.append("OAuthScopes is missing")
|
||||
if token == null:
|
||||
result.append("OAuthToken is missing")
|
||||
return result
|
||||
1
addons/twitcher/auth/twitch_auth.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://iv0mgv0lu8b0
|
||||
6
addons/twitcher/auth/twitch_oauth_scopes.gd
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
@tool
|
||||
@icon("res://addons/twitcher/lib/oOuch/scope-icon.svg")
|
||||
extends OAuthScopes
|
||||
|
||||
## Technically the same like OAuthScopes just with a custom inspector for Twitch scopes
|
||||
class_name TwitchOAuthScopes
|
||||
1
addons/twitcher/auth/twitch_oauth_scopes.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b3n3et8mebjcc
|
||||
195
addons/twitcher/auth/twitch_scope.gd
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
@tool
|
||||
extends Resource
|
||||
|
||||
class_name TwitchScope
|
||||
|
||||
class Definition extends Object:
|
||||
var value: StringName
|
||||
var description: String
|
||||
var categroy: String
|
||||
|
||||
|
||||
func _init(val: StringName, desc: String, cat: String = "") -> void:
|
||||
value = val
|
||||
description = desc
|
||||
categroy = cat
|
||||
|
||||
|
||||
func get_category() -> String:
|
||||
if categroy: return categroy
|
||||
return value.substr(0, value.find(":"))
|
||||
|
||||
|
||||
static var ANALYTICS_READ_EXTENSIONS = Definition.new(&"analytics:read:extensions", "View analytics data for the Twitch Extensions owned by the authenticated account.")
|
||||
static var ANALYTICS_READ_GAMES = Definition.new(&"analytics:read:games", "View analytics data for the games owned by the authenticated account.")
|
||||
static var BITS_READ = Definition.new(&"bits:read", "View Bits information for a channel.")
|
||||
static var CHANNEL_BOT = Definition.new(&"channel:bot", "Joins your channel’s chatroom as a bot user, and perform chat-related actions as that user.")
|
||||
static var CHANNEL_MANAGE_ADS = Definition.new(&"channel:manage:ads", "Manage ads schedule on a channel.")
|
||||
static var CHANNEL_READ_ADS = Definition.new(&"channel:read:ads", "Read the ads schedule and details on your channel.")
|
||||
static var CHANNEL_MANAGE_BROADCAST = Definition.new(&"channel:manage:broadcast", "Manage a channel’s broadcast configuration, including updating channel configuration and managing stream markers and stream tags.")
|
||||
static var CHANNEL_READ_CHARITY = Definition.new(&"channel:read:charity", "Read charity campaign details and user donations on your channel.")
|
||||
static var CHANNEL_EDIT_COMMERCIAL = Definition.new(&"channel:edit:commercial", "Run commercials on a channel.")
|
||||
static var CHANNEL_READ_EDITORS = Definition.new(&"channel:read:editors", "View a list of users with the editor role for a channel.")
|
||||
static var CHANNEL_MANAGE_EXTENSIONS = Definition.new(&"channel:manage:extensions", "Manage a channel’s Extension configuration, including activating Extensions.")
|
||||
static var CHANNEL_READ_GOALS = Definition.new(&"channel:read:goals", "View Creator Goals for a channel.")
|
||||
static var CHANNEL_READ_GUEST_STAR = Definition.new(&"channel:read:guest_star", "Read Guest Star details for your channel.")
|
||||
static var CHANNEL_MANAGE_GUEST_STAR = Definition.new(&"channel:manage:guest_star", "Manage Guest Star for your channel.")
|
||||
static var CHANNEL_READ_HYPE_TRAIN = Definition.new(&"channel:read:hype_train", "View Hype Train information for a channel.")
|
||||
static var CHANNEL_MANAGE_MODERATORS = Definition.new(&"channel:manage:moderators", "Add or remove the moderator role from users in your channel.")
|
||||
static var CHANNEL_READ_POLLS = Definition.new(&"channel:read:polls", "View a channel’s polls.")
|
||||
static var CHANNEL_MANAGE_POLLS = Definition.new(&"channel:manage:polls", "Manage a channel’s polls.")
|
||||
static var CHANNEL_READ_PREDICTIONS = Definition.new(&"channel:read:predictions", "View a channel’s Channel Points Predictions.")
|
||||
static var CHANNEL_MANAGE_PREDICTIONS = Definition.new(&"channel:manage:predictions", "Manage of channel’s Channel Points Predictions")
|
||||
static var CHANNEL_MANAGE_RAIDS = Definition.new(&"channel:manage:raids", "Manage a channel raiding another channel.")
|
||||
static var CHANNEL_READ_REDEMPTIONS = Definition.new(&"channel:read:redemptions", "View Channel Points custom rewards and their redemptions on a channel.")
|
||||
static var CHANNEL_MANAGE_REDEMPTIONS = Definition.new(&"channel:manage:redemptions", "Manage Channel Points custom rewards and their redemptions on a channel.")
|
||||
static var CHANNEL_MANAGE_SCHEDULE = Definition.new(&"channel:manage:schedule", "Manage a channel’s stream schedule.")
|
||||
static var CHANNEL_READ_STREAM_KEY = Definition.new(&"channel:read:stream_key", "View an authorized user’s stream key.")
|
||||
static var CHANNEL_READ_SUBSCRIPTIONS = Definition.new(&"channel:read:subscriptions", "View a list of all subscribers to a channel and check if a user is subscribed to a channel.")
|
||||
static var CHANNEL_MANAGE_VIDEOS = Definition.new(&"channel:manage:videos", "Manage a channel’s videos, including deleting videos.")
|
||||
static var CHANNEL_READ_VIPS = Definition.new(&"channel:read:vips", "Read the list of VIPs in your channel.")
|
||||
static var CHANNEL_MANAGE_VIPS = Definition.new(&"channel:manage:vips", "Add or remove the VIP role from users in your channel.")
|
||||
static var CLIPS_EDIT = Definition.new(&"clips:edit", "Manage Clips for a channel.")
|
||||
static var MODERATION_READ = Definition.new(&"moderation:read", "View a channel’s moderation data including Moderators, Bans, Timeouts, and Automod settings.")
|
||||
static var MODERATOR_MANAGE_ANNOUNCEMENTS = Definition.new(&"moderator:manage:announcements", "Send announcements in channels where you have the moderator role.")
|
||||
static var MODERATOR_MANAGE_AUTOMOD = Definition.new(&"moderator:manage:automod", "Manage messages held for review by AutoMod in channels where you are a moderator.")
|
||||
static var MODERATOR_READ_AUTOMOD_SETTINGS = Definition.new(&"moderator:read:automod_settings", "View a broadcaster’s AutoMod settings.")
|
||||
static var MODERATOR_MANAGE_AUTOMOD_SETTINGS = Definition.new(&"moderator:manage:automod_settings", "Manage a broadcaster’s AutoMod settings.")
|
||||
static var MODERATOR_READ_BANNED_USERS = Definition.new(&"moderator:read:banned_users", "Read the list of bans or unbans in channels where you have the moderator role.")
|
||||
static var MODERATOR_MANAGE_BANNED_USERS = Definition.new(&"moderator:manage:banned_users", "Ban and unban users.")
|
||||
static var MODERATOR_READ_BLOCKED_TERMS = Definition.new(&"moderator:read:blocked_terms", "View a broadcaster’s list of blocked terms.")
|
||||
static var MODERATOR_READ_CHAT_MESSAGES = Definition.new(&"moderator:read:chat_messages", "Read deleted chat messages in channels where you have the moderator role.")
|
||||
static var MODERATOR_MANAGE_BLOCKED_TERMS = Definition.new(&"moderator:manage:blocked_terms", "Manage a broadcaster’s list of blocked terms.")
|
||||
static var MODERATOR_MANAGE_CHAT_MESSAGES = Definition.new(&"moderator:manage:chat_messages", "Delete chat messages in channels where you have the moderator role")
|
||||
static var MODERATOR_READ_CHAT_SETTINGS = Definition.new(&"moderator:read:chat_settings", "View a broadcaster’s chat room settings.")
|
||||
static var MODERATOR_MANAGE_CHAT_SETTINGS = Definition.new(&"moderator:manage:chat_settings", "Manage a broadcaster’s chat room settings.")
|
||||
static var MODERATOR_READ_CHATTERS = Definition.new(&"moderator:read:chatters", "View the chatters in a broadcaster’s chat room.")
|
||||
static var MODERATOR_READ_FOLLOWERS = Definition.new(&"moderator:read:followers", "Read the followers of a broadcaster.")
|
||||
static var MODERATOR_READ_GUEST_STAR = Definition.new(&"moderator:read:guest_star", "Read Guest Star details for channels where you are a Guest Star moderator.")
|
||||
static var MODERATOR_MANAGE_GUEST_STAR = Definition.new(&"moderator:manage:guest_star", "Manage Guest Star for channels where you are a Guest Star moderator.")
|
||||
static var MODERATOR_READ_MODERATORS = Definition.new(&"moderator:read:moderators", "Read the list of moderators in channels where you have the moderator role.")
|
||||
static var MODERATOR_READ_SHIELD_MODE = Definition.new(&"moderator:read:shield_mode", "View a broadcaster’s Shield Mode status.")
|
||||
static var MODERATOR_MANAGE_SHIELD_MODE = Definition.new(&"moderator:manage:shield_mode", "Manage a broadcaster’s Shield Mode status.")
|
||||
static var MODERATOR_READ_SHOUTOUTS = Definition.new(&"moderator:read:shoutouts", "View a broadcaster’s shoutouts.")
|
||||
static var MODERATOR_MANAGE_SHOUTOUTS = Definition.new(&"moderator:manage:shoutouts", "Manage a broadcaster’s shoutouts.")
|
||||
static var MODERATOR_READ_SUSPICIOUS_USERS = Definition.new(&"moderator:read:suspicious_users", "Read chat messages from suspicious users and see users flagged as suspicious in channels where you have the moderator role.")
|
||||
static var MODERATOR_READ_UNBAN_REQUESTS = Definition.new(&"moderator:read:unban_requests", "View a broadcaster’s unban requests.")
|
||||
static var MODERATOR_MANAGE_UNBAN_REQUESTS = Definition.new(&"moderator:manage:unban_requests", "Manage a broadcaster’s unban requests.")
|
||||
static var MODERATOR_READ_VIPS = Definition.new(&"moderator:read:vips", "Read the list of VIPs in channels where you have the moderator role.")
|
||||
static var MODERATOR_READ_WARNINGS = Definition.new(&"moderator:read:warnings", "Read warnings in channels where you have the moderator role.")
|
||||
static var MODERATOR_MANAGE_WARNINGS = Definition.new(&"moderator:manage:warnings", "Warn users in channels where you have the moderator role.")
|
||||
static var USER_BOT = Definition.new(&"user:bot", "Join a specified chat channel as your user and appear as a bot, and perform chat-related actions as your user.")
|
||||
static var USER_EDIT = Definition.new(&"user:edit", "Manage a user object.")
|
||||
static var USER_EDIT_BROADCAST = Definition.new(&"user:edit:broadcast", "View and edit a user’s broadcasting configuration, including Extension configurations.")
|
||||
static var USER_READ_BLOCKED_USERS = Definition.new(&"user:read:blocked_users", "View the block list of a user.")
|
||||
static var USER_MANAGE_BLOCKED_USERS = Definition.new(&"user:manage:blocked_users", "Manage the block list of a user.")
|
||||
static var USER_READ_BROADCAST = Definition.new(&"user:read:broadcast", "View a user’s broadcasting configuration, including Extension configurations.")
|
||||
static var USER_READ_CHAT = Definition.new(&"user:read:chat", "Receive chatroom messages and informational notifications relating to a channel’s chatroom.")
|
||||
static var USER_MANAGE_CHAT_COLOR = Definition.new(&"user:manage:chat_color", "Update the color used for the user’s name in chat.")
|
||||
static var USER_READ_EMAIL = Definition.new(&"user:read:email", "View a user’s email address.")
|
||||
static var USER_READ_EMOTES = Definition.new(&"user:read:emotes", "View emotes available to a user")
|
||||
static var USER_READ_FOLLOWS = Definition.new(&"user:read:follows", "View the list of channels a user follows.")
|
||||
static var USER_READ_MODERATED_CHANNELS = Definition.new(&"user:read:moderated_channels", "Read the list of channels you have moderator privileges in.")
|
||||
static var USER_READ_SUBSCRIPTIONS = Definition.new(&"user:read:subscriptions", "View if an authorized user is subscribed to specific channels.")
|
||||
static var USER_READ_WHISPERS = Definition.new(&"user:read:whispers", "Receive whispers sent to your user.")
|
||||
static var USER_MANAGE_WHISPERS = Definition.new(&"user:manage:whispers", "Receive whispers sent to your user, and send whispers on your user’s behalf.")
|
||||
static var USER_WRITE_CHAT = Definition.new(&"user:write:chat", "Send chat messages to a chatroom.")
|
||||
static var CHAT_READ = Definition.new(&"chat:edit", "Send chat messages to a chatroom using an IRC connection.", "IRC")
|
||||
static var CHAT_EDIT = Definition.new(&"chat:read", "View chat messages sent in a chatroom using an IRC connection.", "IRC")
|
||||
|
||||
## Key: Scope Name as String | Value: Definition
|
||||
static var SCOPE_MAP: Dictionary = {
|
||||
"analytics:read:extensions" = ANALYTICS_READ_EXTENSIONS,
|
||||
"analytics:read:games" = ANALYTICS_READ_GAMES,
|
||||
"bits:read" = BITS_READ,
|
||||
"channel:bot" = CHANNEL_BOT,
|
||||
"channel:manage:ads" = CHANNEL_MANAGE_ADS,
|
||||
"channel:read:ads" = CHANNEL_READ_ADS,
|
||||
"channel:manage:broadcast" = CHANNEL_MANAGE_BROADCAST,
|
||||
"channel:read:charity" = CHANNEL_READ_CHARITY,
|
||||
"channel:edit:commercial" = CHANNEL_EDIT_COMMERCIAL,
|
||||
"channel:read:editors" = CHANNEL_READ_EDITORS,
|
||||
"channel:manage:extensions" = CHANNEL_MANAGE_EXTENSIONS,
|
||||
"channel:read:goals" = CHANNEL_READ_GOALS,
|
||||
"channel:read:guest_star" = CHANNEL_READ_GUEST_STAR,
|
||||
"channel:manage:guest_star" = CHANNEL_MANAGE_GUEST_STAR,
|
||||
"channel:read:hype_train" = CHANNEL_READ_HYPE_TRAIN,
|
||||
"channel:manage:moderators" = CHANNEL_MANAGE_MODERATORS,
|
||||
"channel:read:polls" = CHANNEL_READ_POLLS,
|
||||
"channel:manage:polls" = CHANNEL_MANAGE_POLLS,
|
||||
"channel:read:predictions" = CHANNEL_READ_PREDICTIONS,
|
||||
"channel:manage:predictions" = CHANNEL_MANAGE_PREDICTIONS,
|
||||
"channel:manage:raids" = CHANNEL_MANAGE_RAIDS,
|
||||
"channel:read:redemptions" = CHANNEL_READ_REDEMPTIONS,
|
||||
"channel:manage:redemptions" = CHANNEL_MANAGE_REDEMPTIONS,
|
||||
"channel:manage:schedule" = CHANNEL_MANAGE_SCHEDULE,
|
||||
"channel:read:stream_key" = CHANNEL_READ_STREAM_KEY,
|
||||
"channel:read:subscriptions" = CHANNEL_READ_SUBSCRIPTIONS,
|
||||
"channel:manage:videos" = CHANNEL_MANAGE_VIDEOS,
|
||||
"channel:read:vips" = CHANNEL_READ_VIPS,
|
||||
"channel:manage:vips" = CHANNEL_MANAGE_VIPS,
|
||||
"clips:edit" = CLIPS_EDIT,
|
||||
"moderation:read" = MODERATION_READ,
|
||||
"moderator:manage:announcements" = MODERATOR_MANAGE_ANNOUNCEMENTS,
|
||||
"moderator:manage:automod" = MODERATOR_MANAGE_AUTOMOD,
|
||||
"moderator:read:automod_settings" = MODERATOR_READ_AUTOMOD_SETTINGS,
|
||||
"moderator:manage:automod_settings" = MODERATOR_MANAGE_AUTOMOD_SETTINGS,
|
||||
"moderator:read:banned_users" = MODERATOR_READ_BANNED_USERS,
|
||||
"moderator:manage:banned_users" = MODERATOR_MANAGE_BANNED_USERS,
|
||||
"moderator:read:blocked_terms" = MODERATOR_READ_BLOCKED_TERMS,
|
||||
"moderator:read:chat_messages" = MODERATOR_READ_CHAT_MESSAGES,
|
||||
"moderator:manage:blocked_terms" = MODERATOR_MANAGE_BLOCKED_TERMS,
|
||||
"moderator:manage:chat_messages" = MODERATOR_MANAGE_CHAT_MESSAGES,
|
||||
"moderator:read:chat_settings" = MODERATOR_READ_CHAT_SETTINGS,
|
||||
"moderator:manage:chat_settings" = MODERATOR_MANAGE_CHAT_SETTINGS,
|
||||
"moderator:read:chatters" = MODERATOR_READ_CHATTERS,
|
||||
"moderator:read:followers" = MODERATOR_READ_FOLLOWERS,
|
||||
"moderator:read:guest_star" = MODERATOR_READ_GUEST_STAR,
|
||||
"moderator:manage:guest_star" = MODERATOR_MANAGE_GUEST_STAR,
|
||||
"moderator:read:moderators" = MODERATOR_READ_MODERATORS,
|
||||
"moderator:read:shield_mode" = MODERATOR_READ_SHIELD_MODE,
|
||||
"moderator:manage:shield_mode" = MODERATOR_MANAGE_SHIELD_MODE,
|
||||
"moderator:read:shoutouts" = MODERATOR_READ_SHOUTOUTS,
|
||||
"moderator:manage:shoutouts" = MODERATOR_MANAGE_SHOUTOUTS,
|
||||
"moderator:read:suspicious_users" = MODERATOR_READ_SUSPICIOUS_USERS,
|
||||
"moderator:read:unban_requests" = MODERATOR_READ_UNBAN_REQUESTS,
|
||||
"moderator:manage:unban_requests" = MODERATOR_MANAGE_UNBAN_REQUESTS,
|
||||
"moderator:read:vips" = MODERATOR_READ_VIPS,
|
||||
"moderator:read:warnings" = MODERATOR_READ_WARNINGS,
|
||||
"moderator:manage:warnings" = MODERATOR_MANAGE_WARNINGS,
|
||||
"user:bot" = USER_BOT,
|
||||
"user:edit" = USER_EDIT,
|
||||
"user:edit:broadcast" = USER_EDIT_BROADCAST,
|
||||
"user:read:blocked_users" = USER_READ_BLOCKED_USERS,
|
||||
"user:manage:blocked_users" = USER_MANAGE_BLOCKED_USERS,
|
||||
"user:read:broadcast" = USER_READ_BROADCAST,
|
||||
"user:read:chat" = USER_READ_CHAT,
|
||||
"user:manage:chat_color" = USER_MANAGE_CHAT_COLOR,
|
||||
"user:read:email" = USER_READ_EMAIL,
|
||||
"user:read:emotes" = USER_READ_EMOTES,
|
||||
"user:read:follows" = USER_READ_FOLLOWS,
|
||||
"user:read:moderated_channels" = USER_READ_MODERATED_CHANNELS,
|
||||
"user:read:subscriptions" = USER_READ_SUBSCRIPTIONS,
|
||||
"user:read:whispers" = USER_READ_WHISPERS,
|
||||
"user:manage:whispers" = USER_MANAGE_WHISPERS,
|
||||
"user:write:chat" = USER_WRITE_CHAT,
|
||||
"chat:read" = CHAT_READ,
|
||||
"chat:edit" = CHAT_EDIT,
|
||||
}
|
||||
|
||||
|
||||
## Key: Category as String, value as Array[Definition]
|
||||
static func get_grouped_scopes() -> Dictionary:
|
||||
var result = {}
|
||||
for scope: Definition in get_all_scopes():
|
||||
var category_name = scope.get_category()
|
||||
var category = result.get_or_add(category_name, [])
|
||||
category.append(scope)
|
||||
return result
|
||||
|
||||
|
||||
static func get_all_scopes() -> Array[Definition]:
|
||||
var scopes: Array[Definition] = []
|
||||
for scope in SCOPE_MAP.values():
|
||||
scopes.append(scope)
|
||||
return scopes
|
||||
1
addons/twitcher/auth/twitch_scope.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bjgpdxggorn0l
|
||||
50
addons/twitcher/auth/twitch_token_handler.gd
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
@icon("res://addons/twitcher/assets/auth-icon.svg")
|
||||
@tool
|
||||
extends OAuthTokenHandler
|
||||
|
||||
class_name TwitchTokenHandler
|
||||
|
||||
## Time between checking the validation of the tokens
|
||||
var _last_validation_check: int = 0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
super._ready()
|
||||
# We don't need to check right after the start
|
||||
_last_validation_check = Time.get_ticks_msec() + 60 * 60 * 1000;
|
||||
|
||||
|
||||
func _check_token_refresh() -> void:
|
||||
super._check_token_refresh()
|
||||
|
||||
if _last_validation_check < Time.get_ticks_msec():
|
||||
_validate_token()
|
||||
|
||||
|
||||
## Calles the validation endpoint of Twtich to make sure
|
||||
func _validate_token() -> void:
|
||||
_last_validation_check = Time.get_ticks_msec() + 60 * 60 * 1000;
|
||||
var validation_request = _http_client.request("https://id.twitch.tv/oauth2/validate", HTTPClient.METHOD_GET, {
|
||||
"Authorization": "OAuth %s" % await token.get_access_token()
|
||||
}, "")
|
||||
var response = await _http_client.wait_for_request(validation_request)
|
||||
if response.response_code != 200:
|
||||
refresh_tokens()
|
||||
return
|
||||
|
||||
var response_string: String = response.response_data.get_string_from_utf8();
|
||||
var response_data = JSON.parse_string(response_string);
|
||||
if response_data["expires_in"] <= 0:
|
||||
refresh_tokens()
|
||||
return
|
||||
|
||||
|
||||
func revoke_token() -> void:
|
||||
var request = _http_client.request("https://id.twitch.tv/oauth2/revoke", HTTPClient.METHOD_POST,
|
||||
{ "Content-Type": "application/x-www-form-urlencoded" },
|
||||
"client_id=%s&token=%s" % [oauth_setting.client_id, await token.get_access_token()])
|
||||
var response: BufferedHTTPClient.ResponseData = await _http_client.wait_for_request(request)
|
||||
if response.error:
|
||||
var response_message = response.response_data.get_string_from_utf8()
|
||||
logError("Couldn't revoke Token cause of: %s" % response_message)
|
||||
token.remove_tokens()
|
||||
1
addons/twitcher/auth/twitch_token_handler.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://blnbogtrshw4r
|
||||
14
addons/twitcher/chat/twitch_announcement_color.gd
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
extends RefCounted
|
||||
|
||||
class_name TwitchAnnouncementColor
|
||||
|
||||
static var BLUE: TwitchAnnouncementColor = TwitchAnnouncementColor.new("blue")
|
||||
static var GREEN: TwitchAnnouncementColor = TwitchAnnouncementColor.new("green")
|
||||
static var ORANGE: TwitchAnnouncementColor = TwitchAnnouncementColor.new("orange")
|
||||
static var PURPLE: TwitchAnnouncementColor = TwitchAnnouncementColor.new("purple")
|
||||
static var PRIMARY: TwitchAnnouncementColor = TwitchAnnouncementColor.new("primary")
|
||||
|
||||
var value;
|
||||
|
||||
func _init(color: String) -> void:
|
||||
value = color;
|
||||
1
addons/twitcher/chat/twitch_announcement_color.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://doop8abj8sed6
|
||||
116
addons/twitcher/chat/twitch_chat.gd
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
@icon("../assets/chat-icon.svg")
|
||||
@tool
|
||||
extends Twitcher
|
||||
|
||||
## Grants access to read and write to a chat
|
||||
class_name TwitchChat
|
||||
|
||||
static var _log: TwitchLogger = TwitchLogger.new("TwitchChat")
|
||||
|
||||
static var instance: TwitchChat
|
||||
|
||||
@export var broadcaster_user: TwitchUser:
|
||||
set(val):
|
||||
broadcaster_user = val
|
||||
update_configuration_warnings()
|
||||
## Can be null. Then the owner of the access token will be used to send message aka the current user.
|
||||
@export var sender_user: TwitchUser
|
||||
@export var media_loader: TwitchMediaLoader
|
||||
@export var eventsub: TwitchEventsub:
|
||||
set(val):
|
||||
eventsub = val
|
||||
update_configuration_warnings()
|
||||
@export var api: TwitchAPI:
|
||||
set(val):
|
||||
api = val
|
||||
update_configuration_warnings()
|
||||
|
||||
## Should it subscribe on ready
|
||||
@export var subscribe_on_ready: bool = true
|
||||
|
||||
|
||||
## Triggered when a chat message got received
|
||||
signal message_received(message: TwitchChatMessage)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_log.d("is ready")
|
||||
if media_loader == null: media_loader = TwitchMediaLoader.instance
|
||||
if api == null: api = TwitchAPI.instance
|
||||
if eventsub == null: eventsub = TwitchEventsub.instance
|
||||
eventsub.event.connect(_on_event_received)
|
||||
if not Engine.is_editor_hint() && subscribe_on_ready:
|
||||
subscribe()
|
||||
|
||||
|
||||
func _enter_tree() -> void:
|
||||
if instance == null: instance = self
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if instance == self: instance = null
|
||||
|
||||
|
||||
## Subscribe to eventsub and preload data if not happend yet
|
||||
func subscribe() -> void:
|
||||
if broadcaster_user == null:
|
||||
printerr("BroadcasterUser is not set. Can't subscribe to chat.")
|
||||
return
|
||||
|
||||
if is_instance_valid(media_loader):
|
||||
media_loader.preload_badges(broadcaster_user.id)
|
||||
media_loader.preload_emotes(broadcaster_user.id)
|
||||
|
||||
for subscription: TwitchEventsubConfig in eventsub.get_subscriptions():
|
||||
if subscription.type == TwitchEventsubDefinition.Type.CHANNEL_CHAT_MESSAGE and \
|
||||
subscription.condition.broadcaster_user_id == broadcaster_user.id:
|
||||
# it is already subscribed
|
||||
return
|
||||
|
||||
if sender_user == null:
|
||||
var current_user: TwitchGetUsers.Response = await api.get_users(null)
|
||||
sender_user = current_user.data[0]
|
||||
|
||||
var config: TwitchEventsubConfig = TwitchEventsubConfig.new()
|
||||
config.type = TwitchEventsubDefinition.Type.CHANNEL_CHAT_MESSAGE
|
||||
config.condition = {
|
||||
"broadcaster_user_id": broadcaster_user.id,
|
||||
"user_id": sender_user.id
|
||||
}
|
||||
eventsub.subscribe(config)
|
||||
_log.i("Listen to Chat of %s (%s)" % [broadcaster_user.display_name, broadcaster_user.id])
|
||||
|
||||
|
||||
func _on_event_received(type: StringName, data: Dictionary) -> void:
|
||||
if type != TwitchEventsubDefinition.CHANNEL_CHAT_MESSAGE.value: return
|
||||
var message: TwitchChatMessage = TwitchChatMessage.from_json(data)
|
||||
if message.broadcaster_user_id == broadcaster_user.id:
|
||||
message_received.emit(message)
|
||||
|
||||
|
||||
func send_message(message: String, reply_parent_message_id: String = "") -> Array[TwitchSendChatMessage.ResponseData]:
|
||||
var message_body: TwitchSendChatMessage.Body = TwitchSendChatMessage.Body.new()
|
||||
message_body.broadcaster_id = broadcaster_user.id
|
||||
message_body.sender_id = sender_user.id
|
||||
message_body.message = message
|
||||
if reply_parent_message_id:
|
||||
message_body.reply_parent_message_id = reply_parent_message_id
|
||||
|
||||
var response: TwitchSendChatMessage.Response = await api.send_chat_message(message_body)
|
||||
if _log.enabled:
|
||||
for message_data: TwitchSendChatMessage.ResponseData in response.data:
|
||||
if not message_data.is_sent:
|
||||
_log.w(message_data.drop_reason)
|
||||
|
||||
return response.data
|
||||
|
||||
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var result: PackedStringArray = []
|
||||
if eventsub == null:
|
||||
result.append("TwitchEventsub not assigned")
|
||||
if api == null:
|
||||
result.append("TwitchAPI not assigned")
|
||||
if broadcaster_user == null:
|
||||
result.append("Target broadcaster not specified")
|
||||
return result
|
||||
1
addons/twitcher/chat/twitch_chat.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dcq1bvfrqimqq
|
||||
367
addons/twitcher/chat/twitch_chat_message.gd
Normal file
|
|
@ -0,0 +1,367 @@
|
|||
extends RefCounted
|
||||
|
||||
class_name TwitchChatMessage
|
||||
|
||||
enum FragmentType {
|
||||
text = 0,
|
||||
cheermote = 1,
|
||||
emote = 2,
|
||||
mention = 3
|
||||
}
|
||||
|
||||
const FRAGMENT_TYPES = ["text", "cheermote", "emote", "mention"]
|
||||
|
||||
enum EmoteFormat {
|
||||
animated = 0,
|
||||
_static = 1
|
||||
}
|
||||
|
||||
const EMOTE_FORMATES = ["animated", "static"]
|
||||
|
||||
enum MessageType {
|
||||
## Normal chat message
|
||||
text = 0,
|
||||
## The default reward where the message is highlighted
|
||||
channel_points_highlighted = 1,
|
||||
## Channel points were used to send this message in sub-only mode.
|
||||
channel_points_sub_only = 2,
|
||||
## when a new user is typing for the first time
|
||||
user_intro = 3,
|
||||
## When the power up message effect was used on this message
|
||||
power_ups_message_effect = 4,
|
||||
## When a gigantified emote was posted
|
||||
power_ups_gigantified_emote = 5
|
||||
}
|
||||
|
||||
const MESSAGE_TYPES = ["text", "channel_points_highlighted", "channel_points_sub_only", "user_intro", "power_ups_message_effect", "power_ups_gigantified_emote"]
|
||||
|
||||
class Message extends RefCounted:
|
||||
## The chat message in plain text.
|
||||
var text: String
|
||||
## Ordered list of chat message fragments.
|
||||
var fragments: Array[Fragment] = []
|
||||
|
||||
|
||||
static func from_json(d: Dictionary) -> Message:
|
||||
var result = Message.new()
|
||||
if d.has("text") and d["text"] != null:
|
||||
result.text = d["text"]
|
||||
if d.has("fragments") and d["fragments"] != null:
|
||||
for value in d["fragments"]:
|
||||
result.fragments.append(Fragment.from_json(value))
|
||||
return result
|
||||
|
||||
|
||||
class Fragment extends RefCounted:
|
||||
## The type of message fragment. See "TwitchChatMessage.FRAGMENT_TYPE_*"
|
||||
var type: MessageType
|
||||
## Message text in fragment.
|
||||
var text: String
|
||||
## Optional. Metadata pertaining to the cheermote.
|
||||
var cheermote: Cheermote
|
||||
## Optional. Metadata pertaining to the emote.
|
||||
var emote: Emote
|
||||
## Optional. Metadata pertaining to the mention.
|
||||
var mention: Mention
|
||||
|
||||
|
||||
static func from_json(d: Dictionary) -> Fragment:
|
||||
var result = Fragment.new()
|
||||
if d.has("type") and d["type"] != null:
|
||||
result.type = FragmentType[d["type"]]
|
||||
if d.has("text") and d["text"] != null:
|
||||
result.text = d["text"]
|
||||
if d.has("cheermote") and d["cheermote"] != null:
|
||||
result.cheermote = Cheermote.from_json(d["cheermote"])
|
||||
if d.has("emote") and d["emote"] != null:
|
||||
result.emote = Emote.from_json(d["emote"])
|
||||
if d.has("mention") and d["mention"] != null:
|
||||
result.mention = Mention.from_json(d["mention"])
|
||||
return result
|
||||
|
||||
|
||||
class Mention extends RefCounted:
|
||||
## The user ID of the mentioned user.
|
||||
var user_id: String
|
||||
## The user name of the mentioned user.
|
||||
var user_name: String
|
||||
## The user login of the mentioned user.
|
||||
var user_login: String
|
||||
|
||||
|
||||
static func from_json(d: Dictionary) -> Mention:
|
||||
var result = Mention.new()
|
||||
if d.has("user_id") and d["user_id"] != null:
|
||||
result.user_id = d["user_id"]
|
||||
if d.has("user_name") and d["user_name"] != null:
|
||||
result.user_name = d["user_name"]
|
||||
if d.has("user_login") and d["user_login"] != null:
|
||||
result.user_login = d["user_login"]
|
||||
return result
|
||||
|
||||
|
||||
class Cheermote extends RefCounted:
|
||||
## The name portion of the Cheermote string that you use in chat to cheer Bits. The full Cheermote string is the concatenation of {prefix} + {number of Bits}. For example, if the prefix is “Cheer” and you want to cheer 100 Bits, the full Cheermote string is Cheer100. When the Cheermote string is entered in chat, Twitch converts it to the image associated with the Bits tier that was cheered.
|
||||
var prefix: String
|
||||
## The amount of bits cheered.
|
||||
var bits: int
|
||||
## The tier level of the cheermote.
|
||||
var tier: int
|
||||
|
||||
|
||||
func get_sprite_frames(_media_loader: TwitchMediaLoader, cheermote_definition: TwitchCheermoteDefinition) -> SpriteFrames:
|
||||
var cheer_results = await _media_loader.get_cheer_tier(prefix, "%s" % tier, cheermote_definition.theme, cheermote_definition.type, cheermote_definition.scale)
|
||||
return cheer_results.spriteframes
|
||||
|
||||
|
||||
static func from_json(d: Dictionary) -> Cheermote:
|
||||
var result = Cheermote.new()
|
||||
if d.has("prefix") and d["prefix"] != null:
|
||||
result.prefix = d["prefix"]
|
||||
if d.has("bits") and d["bits"] != null:
|
||||
result.bits = d["bits"]
|
||||
if d.has("tier") and d["tier"] != null:
|
||||
result.tier = d["tier"]
|
||||
return result
|
||||
|
||||
|
||||
class Emote extends RefCounted:
|
||||
## An ID that uniquely identifies this emote.
|
||||
var id: String
|
||||
## An ID that identifies the emote set that the emote belongs to.
|
||||
var emote_set_id: String
|
||||
## The ID of the broadcaster who owns the emote.
|
||||
var owner_id: String
|
||||
## The formats that the emote is available in. For example, if the emote is available only as a static PNG, the array contains only static. But if the emote is available as a static PNG and an animated GIF, the array contains static and animated. See: "TwitchChatMessage.EMOTE_TYPE_*"
|
||||
var format: Array[EmoteFormat] = []
|
||||
|
||||
|
||||
## Resolves the spriteframes from this emote. Check `format` for possible formats.
|
||||
## Format: Defaults to animated when not available it uses static
|
||||
## Scale: 1, 2, 3
|
||||
func get_sprite_frames(_media_loader: TwitchMediaLoader, format: String = "", scale: int = 1, dark: bool = true) -> SpriteFrames:
|
||||
var definition: TwitchEmoteDefinition = TwitchEmoteDefinition.new(id)
|
||||
if dark: definition.theme_dark()
|
||||
else: definition.theme_light()
|
||||
match scale:
|
||||
1: definition.scale_1()
|
||||
2: definition.scale_2()
|
||||
3: definition.scale_3()
|
||||
_: definition.scale_1()
|
||||
var emotes = await _media_loader.get_emotes_by_definition([definition])
|
||||
return emotes[definition]
|
||||
|
||||
|
||||
static func from_json(d: Dictionary) -> Emote:
|
||||
var result = Emote.new()
|
||||
if d.has("id") and d["id"] != null:
|
||||
result.id = d["id"]
|
||||
if d.has("emote_set_id") and d["emote_set_id"] != null:
|
||||
result.emote_set_id = d["emote_set_id"]
|
||||
if d.has("owner_id") and d["owner_id"] != null:
|
||||
result.owner_id = d["owner_id"]
|
||||
if d.has("format") and d["format"] != null:
|
||||
for format in d["format"]:
|
||||
if format == "static":
|
||||
result.format.append(EmoteFormat._static)
|
||||
elif format == "animated":
|
||||
result.format.append(EmoteFormat.animated)
|
||||
return result
|
||||
|
||||
|
||||
|
||||
|
||||
class Badge extends RefCounted:
|
||||
## An ID that identifies this set of chat badges. For example, Bits or Subscriber.
|
||||
var set_id: String
|
||||
## An ID that identifies this version of the badge. The ID can be any value. For example, for Bits, the ID is the Bits tier level, but for World of Warcraft, it could be Alliance or Horde.
|
||||
var id: String
|
||||
## Contains metadata related to the chat badges in the badges tag. Currently, this tag contains metadata only for subscriber badges, to indicate the number of months the user has been a subscriber.
|
||||
var info: String
|
||||
|
||||
static func from_json(d: Dictionary) -> Badge:
|
||||
var result = Badge.new()
|
||||
if d.has("set_id") and d["set_id"] != null:
|
||||
result.set_id = d["set_id"]
|
||||
if d.has("id") and d["id"] != null:
|
||||
result.id = d["id"]
|
||||
if d.has("info") and d["info"] != null:
|
||||
result.info = d["info"]
|
||||
return result
|
||||
|
||||
|
||||
|
||||
class Cheer extends RefCounted:
|
||||
## The amount of Bits the user cheered.
|
||||
var bits: int
|
||||
|
||||
|
||||
static func from_json(d: Dictionary) -> Cheer:
|
||||
var result = Cheer.new()
|
||||
if d.has("bits") and d["bits"] != null:
|
||||
result.bits = d["bits"]
|
||||
return result
|
||||
|
||||
class Reply extends RefCounted:
|
||||
## An ID that uniquely identifies the parent message that this message is replying to.
|
||||
var parent_message_id: String
|
||||
## The message body of the parent message.
|
||||
var parent_message_body: String
|
||||
## User ID of the sender of the parent message.
|
||||
var parent_user_id: String
|
||||
## User name of the sender of the parent message.
|
||||
var parent_user_name: String
|
||||
## User login of the sender of the parent message.
|
||||
var parent_user_login: String
|
||||
## An ID that identifies the parent message of the reply thread.
|
||||
var thread_message_id: String
|
||||
## User ID of the sender of the thread’s parent message.
|
||||
var thread_user_id: String
|
||||
## User name of the sender of the thread’s parent message.
|
||||
var thread_user_name: String
|
||||
## User login of the sender of the thread’s parent message.
|
||||
var thread_user_login: String
|
||||
|
||||
static func from_json(d: Dictionary) -> Reply:
|
||||
var result = Reply.new()
|
||||
if d.has("parent_message_id") and d["parent_message_id"] != null:
|
||||
result.parent_message_id = d["parent_message_id"]
|
||||
if d.has("parent_message_body") and d["parent_message_body"] != null:
|
||||
result.parent_message_body = d["parent_message_body"]
|
||||
if d.has("parent_user_id") and d["parent_user_id"] != null:
|
||||
result.parent_user_id = d["parent_user_id"]
|
||||
if d.has("parent_user_name") and d["parent_user_name"] != null:
|
||||
result.parent_user_name = d["parent_user_name"]
|
||||
if d.has("parent_user_login") and d["parent_user_login"] != null:
|
||||
result.parent_user_login = d["parent_user_login"]
|
||||
if d.has("thread_message_id") and d["thread_message_id"] != null:
|
||||
result.thread_message_id = d["thread_message_id"]
|
||||
if d.has("thread_user_id") and d["thread_user_id"] != null:
|
||||
result.thread_user_id = d["thread_user_id"]
|
||||
if d.has("thread_user_name") and d["thread_user_name"] != null:
|
||||
result.thread_user_name = d["thread_user_name"]
|
||||
if d.has("thread_user_login") and d["thread_user_login"] != null:
|
||||
result.thread_user_login = d["thread_user_login"]
|
||||
return result
|
||||
|
||||
|
||||
## The broadcaster user ID.
|
||||
var broadcaster_user_id: String
|
||||
## The broadcaster display name.
|
||||
var broadcaster_user_name: String
|
||||
## The broadcaster login.
|
||||
var broadcaster_user_login: String
|
||||
## The user ID of the user that sent the message.
|
||||
var chatter_user_id: String
|
||||
## The user name of the user that sent the message.
|
||||
var chatter_user_name: String
|
||||
## The user login of the user that sent the message.
|
||||
var chatter_user_login: String
|
||||
## A UUID that identifies the message.
|
||||
var message_id: String
|
||||
## The structured chat message.
|
||||
var message: Message
|
||||
## The type of message.
|
||||
var message_type: MessageType
|
||||
## List of chat badges.
|
||||
var badges: Array[Badge] = []
|
||||
## Optional. Metadata if this message is a cheer.
|
||||
var cheer: Cheer
|
||||
## The color of the user’s name in the chat room. This is a hexadecimal RGB color code in the form, #<RGB>;. This tag may be empty if it is never set.
|
||||
var color: String
|
||||
## Optional. Metadata if this message is a reply.
|
||||
var reply: Reply
|
||||
## Optional. The ID of a channel points custom reward that was redeemed.
|
||||
var channel_points_custom_reward_id: String
|
||||
## Optional. The broadcaster user ID of the channel the message was sent from. Is null when the message happens in the same channel as the broadcaster. Is not null when in a shared chat session, and the action happens in the channel of a participant other than the broadcaster.
|
||||
var source_broadcaster_user_id: String
|
||||
## Optional. The user name of the broadcaster of the channel the message was sent from. Is null when the message happens in the same channel as the broadcaster. Is not null when in a shared chat session, and the action happens in the channel of a participant other than the broadcaster.
|
||||
var source_broadcaster_user_name: String
|
||||
## Optional. The login of the broadcaster of the channel the message was sent from. Is null when the message happens in the same channel as the broadcaster. Is not null when in a shared chat session, and the action happens in the channel of a participant other than the broadcaster.
|
||||
var source_broadcaster_user_login: String
|
||||
## Optional. The UUID that identifies the source message from the channel the message was sent from. Is null when the message happens in the same channel as the broadcaster. Is not null when in a shared chat session, and the action happens in the channel of a participant other than the broadcaster.
|
||||
var source_message_id: String
|
||||
## Optional. The list of chat badges for the chatter in the channel the message was sent from. Is null when the message happens in the same channel as the broadcaster. Is not null when in a shared chat session, and the action happens in the channel of a participant other than the broadcaster.
|
||||
var source_badges: Array[Badge] = []
|
||||
|
||||
|
||||
## Loads a chat message from Json decoded dictionary. TwitchService is optional in case images and badges should be load from the message.
|
||||
static func from_json(d: Dictionary) -> TwitchChatMessage:
|
||||
var result = TwitchChatMessage.new()
|
||||
if d.has("broadcaster_user_id") and d["broadcaster_user_id"] != null:
|
||||
result.broadcaster_user_id = d["broadcaster_user_id"]
|
||||
if d.has("broadcaster_user_name") and d["broadcaster_user_name"] != null:
|
||||
result.broadcaster_user_name = d["broadcaster_user_name"]
|
||||
if d.has("broadcaster_user_login") and d["broadcaster_user_login"] != null:
|
||||
result.broadcaster_user_login = d["broadcaster_user_login"]
|
||||
if d.has("chatter_user_id") and d["chatter_user_id"] != null:
|
||||
result.chatter_user_id = d["chatter_user_id"]
|
||||
if d.has("chatter_user_name") and d["chatter_user_name"] != null:
|
||||
result.chatter_user_name = d["chatter_user_name"]
|
||||
if d.has("chatter_user_login") and d["chatter_user_login"] != null:
|
||||
result.chatter_user_login = d["chatter_user_login"]
|
||||
if d.has("message_id") and d["message_id"] != null:
|
||||
result.message_id = d["message_id"]
|
||||
if d.has("message") and d["message"] != null:
|
||||
result.message = Message.from_json(d["message"])
|
||||
if d.has("message_type") and d["message_type"] != null:
|
||||
result.message_type = MessageType[d["message_type"]]
|
||||
if d.has("badges") and d["badges"] != null:
|
||||
for value in d["badges"]:
|
||||
result.badges.append(Badge.from_json(value))
|
||||
if d.has("cheer") and d["cheer"] != null:
|
||||
result.cheer = Cheer.from_json(d["cheer"])
|
||||
if d.has("color") and d["color"] != null:
|
||||
result.color = d["color"]
|
||||
if d.has("reply") and d["reply"] != null:
|
||||
result.reply = Reply.from_json(d["reply"])
|
||||
if d.has("channel_points_custom_reward_id") and d["channel_points_custom_reward_id"] != null:
|
||||
result.channel_points_custom_reward_id = d["channel_points_custom_reward_id"]
|
||||
if d.has("source_broadcaster_user_id") and d["source_broadcaster_user_id"] != null:
|
||||
result.source_broadcaster_user_id = d["source_broadcaster_user_id"]
|
||||
if d.has("source_broadcaster_user_name") and d["source_broadcaster_user_name"] != null:
|
||||
result.source_broadcaster_user_name = d["source_broadcaster_user_name"]
|
||||
if d.has("source_broadcaster_user_login") and d["source_broadcaster_user_login"] != null:
|
||||
result.source_broadcaster_user_login = d["source_broadcaster_user_login"]
|
||||
if d.has("source_message_id") and d["source_message_id"] != null:
|
||||
result.source_message_id = d["source_message_id"]
|
||||
if d.has("source_badges") and d["source_badges"] != null:
|
||||
for value in d["source_badges"]:
|
||||
result.source_badges.append(Badge.from_json(value))
|
||||
return result
|
||||
|
||||
|
||||
## Key: TwitchBadgeDefinition | Value: SpriteFrames
|
||||
func get_badges(_media_loader: TwitchMediaLoader, scale: int = 1) -> Dictionary[TwitchBadgeDefinition, SpriteFrames]:
|
||||
var definitions : Array[TwitchBadgeDefinition] = []
|
||||
for badge in badges:
|
||||
var badge_definition : TwitchBadgeDefinition = TwitchBadgeDefinition.new(badge.set_id, badge.id, scale, broadcaster_user_id)
|
||||
definitions.append(badge_definition)
|
||||
var emotes : Dictionary[TwitchBadgeDefinition, SpriteFrames] = await _media_loader.get_badges(definitions)
|
||||
return emotes
|
||||
|
||||
|
||||
## Key: TwitchBadgeDefinition | Value: SpriteFrames
|
||||
func get_source_badges(_media_loader: TwitchMediaLoader, scale: int = 1) -> Dictionary[TwitchBadgeDefinition, SpriteFrames]:
|
||||
var definitions : Array[TwitchBadgeDefinition] = []
|
||||
for badge in source_badges:
|
||||
var badge_definition : TwitchBadgeDefinition = TwitchBadgeDefinition.new(badge.set_id, badge.id, scale, broadcaster_user_id)
|
||||
definitions.append(badge_definition)
|
||||
var emotes : Dictionary[TwitchBadgeDefinition, SpriteFrames] = await _media_loader.get_badges(definitions)
|
||||
return emotes
|
||||
|
||||
## Returns a the color of the user or the default when its not set never null
|
||||
func get_color(default_color: String = "#AAAAAA") -> String:
|
||||
return default_color if color == null || color == "" else color
|
||||
|
||||
|
||||
## Preload all emojis in parallel to reduce loadtime
|
||||
func load_emotes_from_fragment(_media_loader: TwitchMediaLoader) -> Dictionary[TwitchEmoteDefinition, SpriteFrames]:
|
||||
var emotes_to_load : Array[TwitchEmoteDefinition] = []
|
||||
|
||||
for fragment : TwitchChatMessage.Fragment in message.fragments:
|
||||
match fragment.type:
|
||||
TwitchChatMessage.FragmentType.emote:
|
||||
var definition : TwitchEmoteDefinition = TwitchEmoteDefinition.new(fragment.emote.id)
|
||||
emotes_to_load.append(definition)
|
||||
return await _media_loader.get_emotes_by_definition(emotes_to_load)
|
||||
1
addons/twitcher/chat/twitch_chat_message.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bxu8no18dq2e3
|
||||
173
addons/twitcher/chat/twitch_command.gd
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
@icon("res://addons/twitcher/assets/command-icon.svg")
|
||||
extends Twitcher
|
||||
|
||||
# Untested yet
|
||||
## A single command like !lurk
|
||||
class_name TwitchCommand
|
||||
|
||||
static var ALL_COMMANDS: Array[TwitchCommand] = []
|
||||
|
||||
## Called when the command got received in the right format
|
||||
signal command_received(from_username: String, info: TwitchCommandInfo, args: PackedStringArray)
|
||||
|
||||
## Called when the command got received in the wrong format
|
||||
signal received_invalid_command(from_username: String, info: TwitchCommandInfo, args: PackedStringArray)
|
||||
|
||||
## Required permission to execute the command
|
||||
enum PermissionFlag {
|
||||
EVERYONE = 0,
|
||||
VIP = 1,
|
||||
SUB = 2,
|
||||
MOD = 4,
|
||||
STREAMER = 8,
|
||||
MOD_STREAMER = 12, # Mods and the streamer
|
||||
NON_REGULAR = 15 # Everyone but regular viewers
|
||||
}
|
||||
|
||||
## Where the command should be accepted
|
||||
enum WhereFlag {
|
||||
CHAT = 1,
|
||||
WHISPER = 2,
|
||||
ANYWHERE = 3
|
||||
}
|
||||
|
||||
@export var command_prefixes : Array[String] = ["!"]
|
||||
## Name Command
|
||||
@export var command: String
|
||||
## Optional names of commands
|
||||
@export var aliases: Array[String]
|
||||
## Description for the user
|
||||
@export_multiline var description: String
|
||||
|
||||
## Minimal amount of argument 0 means no argument needed
|
||||
@export var args_min: int = 0
|
||||
## Max amount of arguments -1 means infinite
|
||||
@export var args_max: int = -1
|
||||
## Wich role of user is allowed to use it
|
||||
@export var permission_level: PermissionFlag = PermissionFlag.EVERYONE
|
||||
## Where is it allowed to use chat or whisper or both
|
||||
@export var where: WhereFlag = WhereFlag.CHAT
|
||||
## All allowed users empty array means everyone
|
||||
@export var allowed_users: Array[String] = []
|
||||
## All chatrooms where the command listens to
|
||||
@export var listen_to_chatrooms: Array[String] = []
|
||||
|
||||
## The eventsub to listen for chatmessages
|
||||
@export var eventsub: TwitchEventsub
|
||||
|
||||
static func create(
|
||||
eventsub: TwitchEventsub,
|
||||
cmd_name: String,
|
||||
callable: Callable,
|
||||
min_args: int = 0,
|
||||
max_args: int = 0,
|
||||
permission_level: int = PermissionFlag.EVERYONE,
|
||||
where: int = WhereFlag.CHAT,
|
||||
allowed_users: Array[String] = [],
|
||||
listen_to_chatrooms: Array[String] = []) -> TwitchCommand:
|
||||
var command := TwitchCommand.new()
|
||||
command.eventsub = eventsub
|
||||
command.command = cmd_name
|
||||
command.command_received.connect(callable)
|
||||
command.args_min = min_args
|
||||
command.args_max = max_args
|
||||
command.permission_level = permission_level
|
||||
command.where = where
|
||||
command.allowed_users = allowed_users
|
||||
command.listen_to_chatrooms = listen_to_chatrooms
|
||||
return command
|
||||
|
||||
|
||||
func _enter_tree() -> void:
|
||||
if eventsub == null: eventsub = TwitchEventsub.instance
|
||||
eventsub.event.connect(_on_event)
|
||||
ALL_COMMANDS.append(self)
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
eventsub.event.disconnect(_on_event)
|
||||
ALL_COMMANDS.erase(self)
|
||||
|
||||
|
||||
func _on_event(type: StringName, data: Dictionary) -> void:
|
||||
if type == TwitchEventsubDefinition.CHANNEL_CHAT_MESSAGE.value:
|
||||
if where & WhereFlag.CHAT != WhereFlag.CHAT: return
|
||||
var message : String = data.message.text
|
||||
var username : String = data.chatter_user_login
|
||||
var channel_name : String = data.broadcaster_user_login
|
||||
if not _should_handle(message, username, channel_name): return
|
||||
var chat_message = TwitchChatMessage.from_json(data)
|
||||
_handle_command(username, message, channel_name, chat_message)
|
||||
|
||||
if type == TwitchEventsubDefinition.USER_WHISPER_MESSAGE.value:
|
||||
if where & WhereFlag.WHISPER != WhereFlag.WHISPER: return
|
||||
var message : String = data.whisper.text
|
||||
var from_user : String = data.from_user_login
|
||||
if not _should_handle(message, from_user, from_user): return
|
||||
_handle_command(from_user, message, data.to_user_login, data)
|
||||
|
||||
|
||||
func add_alias(alias: String) -> void:
|
||||
aliases.append(alias)
|
||||
|
||||
|
||||
func _should_handle(message: String, username: String, channel_name: String) -> bool:
|
||||
if not listen_to_chatrooms.is_empty() && not listen_to_chatrooms.has(channel_name): return false
|
||||
if not allowed_users.is_empty() && not allowed_users.has(username): return false
|
||||
if not command_prefixes.has(message.left(1)): return false
|
||||
|
||||
# remove the command symbol in front
|
||||
message = message.right(-1)
|
||||
var split : PackedStringArray = message.split(" ", true, 1)
|
||||
var current_command := split[0]
|
||||
if current_command != command && not aliases.has(current_command): return false
|
||||
return true
|
||||
|
||||
|
||||
func _handle_command(from_username: String, raw_message: String, to_user: String, data: Variant) -> void:
|
||||
# remove the command symbol in front
|
||||
raw_message = raw_message.right(-1)
|
||||
var cmd_msg = raw_message.split(" ", true, 1)
|
||||
var message = ""
|
||||
var arg_array : PackedStringArray = []
|
||||
var command = cmd_msg[0]
|
||||
var info = TwitchCommandInfo.new(self, to_user, from_username, arg_array, data)
|
||||
if cmd_msg.size() > 1:
|
||||
message = cmd_msg[1]
|
||||
arg_array.append_array(message.split(" ", false))
|
||||
var to_less_arguments = arg_array.size() < args_min
|
||||
var to_much_arguments = arg_array.size() > args_max
|
||||
if to_much_arguments && args_max != -1 || to_less_arguments:
|
||||
received_invalid_command.emit(from_username, info, arg_array)
|
||||
return
|
||||
var premission_required = permission_level != 0
|
||||
if premission_required:
|
||||
var user_perm_flags = _get_perm_flag_from_tags(data)
|
||||
if user_perm_flags & permission_level == 0:
|
||||
received_invalid_command.emit(from_username, info, arg_array)
|
||||
return
|
||||
if arg_array.size() == 0:
|
||||
if args_min > 0:
|
||||
received_invalid_command.emit(from_username, info, arg_array)
|
||||
return
|
||||
|
||||
var empty_args: Array[String] = []
|
||||
if args_max > 0:
|
||||
command_received.emit(from_username, info, empty_args)
|
||||
else:
|
||||
command_received.emit(from_username, info, empty_args)
|
||||
else:
|
||||
command_received.emit(from_username, info, arg_array)
|
||||
|
||||
|
||||
func _get_perm_flag_from_tags(data : Variant) -> int:
|
||||
var flag: int = 0
|
||||
if data is TwitchChatMessage:
|
||||
var message: TwitchChatMessage = data as TwitchChatMessage
|
||||
for badge in message.badges:
|
||||
match badge.set_id:
|
||||
"broadcaster": flag += PermissionFlag.STREAMER
|
||||
"vip": flag += PermissionFlag.VIP
|
||||
"moderator": flag += PermissionFlag.MOD
|
||||
"subscriber": flag += PermissionFlag.SUB
|
||||
return flag
|
||||
1
addons/twitcher/chat/twitch_command.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bmluckfvgm1c2
|
||||
83
addons/twitcher/chat/twitch_command_help.gd
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
@icon("res://addons/twitcher/assets/command-icon.svg")
|
||||
extends TwitchCommand
|
||||
|
||||
class_name TwitchCommandHelp
|
||||
|
||||
## Used to determine the Sender User if empty and to send the message back
|
||||
@export var twitch_api: TwitchAPI
|
||||
## Sender User that will send the answers on the command. Can be empty then the current user will be used
|
||||
@export var sender_user: TwitchUser
|
||||
|
||||
var _current_user: TwitchUser
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
if command == "": command = "help"
|
||||
|
||||
command_received.connect(_on_command_receive)
|
||||
if twitch_api == null: twitch_api = TwitchAPI.instance
|
||||
if twitch_api == null:
|
||||
push_error("Command is missing TwitchAPI to answer!")
|
||||
return
|
||||
|
||||
var response: TwitchGetUsers.Response = await twitch_api.get_users(TwitchGetUsers.Opt.new())
|
||||
_current_user = response.data[0]
|
||||
if sender_user == null: sender_user = _current_user
|
||||
|
||||
|
||||
func _on_command_receive(from_username: String, info: TwitchCommandInfo, args: PackedStringArray) -> void:
|
||||
if info.original_message is TwitchChatMessage:
|
||||
var help_message: String = _generate_help_message(args, false)
|
||||
var chat_message: TwitchChatMessage = info.original_message as TwitchChatMessage
|
||||
var message_body: TwitchSendChatMessage.Body = TwitchSendChatMessage.Body.new()
|
||||
message_body.broadcaster_id = chat_message.broadcaster_user_id
|
||||
message_body.sender_id = sender_user.id
|
||||
message_body.message = help_message
|
||||
message_body.reply_parent_message_id = chat_message.message_id
|
||||
twitch_api.send_chat_message(message_body)
|
||||
else:
|
||||
var help_message: String = _generate_help_message(args, true)
|
||||
var message: Dictionary = info.original_message
|
||||
if message["to_user_id"] != _current_user.id:
|
||||
push_error("Can't answer the whisper message receiver is not the user that will be used as sender!")
|
||||
return
|
||||
var message_body: TwitchSendWhisper.Body = TwitchSendWhisper.Body.new()
|
||||
message_body.message = help_message
|
||||
twitch_api.send_whisper(message_body, message["to_user_id"], message["from_user_id"])
|
||||
|
||||
|
||||
func _generate_help_message(args: Array[String], whisper_only: bool) -> String:
|
||||
var message: String = ""
|
||||
var show_details: bool = not args.is_empty()
|
||||
|
||||
for command in TwitchCommand.ALL_COMMANDS:
|
||||
if command == self: continue
|
||||
var should_be_added: bool = command.where == TwitchCommand.WhereFlag.ANYWHERE \
|
||||
|| command.where == TwitchCommand.WhereFlag.WHISPER && whisper_only \
|
||||
|| command.where == TwitchCommand.WhereFlag.CHAT && not whisper_only
|
||||
|
||||
if not args.is_empty():
|
||||
should_be_added = should_be_added && _is_command_in_args(command, args)
|
||||
|
||||
if should_be_added:
|
||||
if show_details:
|
||||
message += "[%s%s - %s] " % [command.command_prefixes[0], command.command, command.description]
|
||||
else:
|
||||
message += "%s%s, " % [command.command_prefixes[0], command.command]
|
||||
|
||||
if message == "":
|
||||
return "No commands registered"
|
||||
elif not show_details:
|
||||
message = message.trim_suffix(", ")
|
||||
message = "List of all Commands: %s | You can use '!help COMMAND' for details!" % message
|
||||
return message
|
||||
|
||||
|
||||
func _is_command_in_args(command: TwitchCommand, args: Array[String]) -> bool:
|
||||
for arg in args:
|
||||
if command.command == arg:
|
||||
return true
|
||||
if command.aliases.has(arg):
|
||||
return true
|
||||
return false
|
||||
|
||||
1
addons/twitcher/chat/twitch_command_help.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://ch0rxi1ogjx3q
|
||||
25
addons/twitcher/chat/twitch_command_info.gd
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
extends RefCounted
|
||||
|
||||
## Meta information about the command sender
|
||||
class_name TwitchCommandInfo
|
||||
|
||||
|
||||
var command : TwitchCommand
|
||||
var channel_name : String
|
||||
var username : String
|
||||
var arguments : Array[String]
|
||||
## Depending on the type it's either a TwitchChatMessage or a Dictionary of the whisper message data
|
||||
var original_message : Variant
|
||||
|
||||
|
||||
func _init(
|
||||
_command: TwitchCommand,
|
||||
_channel_name: String,
|
||||
_username: String,
|
||||
_arguments: Array[String],
|
||||
_original_message: Variant):
|
||||
command = _command
|
||||
channel_name = _channel_name
|
||||
username = _username
|
||||
arguments = _arguments
|
||||
original_message = _original_message
|
||||
1
addons/twitcher/chat/twitch_command_info.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c5k8j4ag3n0su
|
||||
10
addons/twitcher/default_oauth_token.tres
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
[gd_resource type="Resource" script_class="OAuthToken" load_steps=3 format=3 uid="uid://m7epy882axmp"]
|
||||
|
||||
[ext_resource type="Resource" uid="uid://c4scwuk8q0r40" path="res://addons/twitcher/lib/oOuch/default_key_provider.tres" id="1_byqke"]
|
||||
[ext_resource type="Script" uid="uid://b52xp7c23ucfk" path="res://addons/twitcher/lib/oOuch/oauth_token.gd" id="2_phbii"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("2_phbii")
|
||||
_crypto_key_provider = ExtResource("1_byqke")
|
||||
_identifier = "EditorToken"
|
||||
_cache_path = "user://auth.conf"
|
||||
606
addons/twitcher/editor/api_generator/twitch_api_generator.gd
Normal file
|
|
@ -0,0 +1,606 @@
|
|||
@icon("res://addons/twitcher/assets/api-icon.svg")
|
||||
@tool
|
||||
extends Twitcher
|
||||
|
||||
class_name TwitchAPIGenerator
|
||||
|
||||
const suffixes: Array[String] = ["Response", "Body", "Opt"]
|
||||
|
||||
const api_output_path = "res://addons/twitcher/generated/"
|
||||
const twitch_api_header : String = """@tool
|
||||
extends Twitcher
|
||||
|
||||
# CLASS GOT AUTOGENERATED DON'T CHANGE MANUALLY. CHANGES CAN BE OVERWRITTEN EASILY.
|
||||
|
||||
## Interaction with the Twitch REST API.
|
||||
class_name TwitchAPI
|
||||
|
||||
static var _log: TwitchLogger = TwitchLogger.new("TwitchAPI")
|
||||
|
||||
static var instance: TwitchAPI
|
||||
|
||||
## Maximal tries to reauthrorize before giving up the request.
|
||||
const MAX_AUTH_ERRORS = 3
|
||||
|
||||
## Called when the API returns unauthenticated mostly cause the accesstoken is expired
|
||||
signal unauthenticated
|
||||
|
||||
## Called when the API returns 403 means there are permissions / scopes missing
|
||||
signal unauthorized
|
||||
|
||||
## To authorize against the Twitch API
|
||||
@export var token: OAuthToken:
|
||||
set(val):
|
||||
token = val
|
||||
update_configuration_warnings()
|
||||
## OAuth settings needed for client information
|
||||
@export var oauth_setting: OAuthSetting:
|
||||
set(val):
|
||||
oauth_setting = val
|
||||
update_configuration_warnings()
|
||||
## URI to the Twitch API
|
||||
@export var api_host: String = "https://api.twitch.tv/helix"
|
||||
|
||||
## Client to make HTTP requests
|
||||
var client: BufferedHTTPClient
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
client = BufferedHTTPClient.new()
|
||||
client.name = "ApiClient"
|
||||
add_child(client)
|
||||
|
||||
|
||||
func _enter_tree() -> void:
|
||||
if instance == null: instance = self
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if instance == self: instance = null
|
||||
|
||||
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var result: PackedStringArray = []
|
||||
if token == null:
|
||||
result.append("Please set a token to use")
|
||||
if oauth_setting == null:
|
||||
result.append("Please set the correct oauth settings")
|
||||
return result
|
||||
|
||||
|
||||
func request(path: String, method: int, body: Variant = "", content_type: String = "", error_count: int = 0) -> BufferedHTTPClient.ResponseData:
|
||||
var header : Dictionary = {
|
||||
"Authorization": "Bearer %s" % [await token.get_access_token()],
|
||||
"Client-ID": oauth_setting.client_id
|
||||
}
|
||||
if content_type != "":
|
||||
header["Content-Type"] = content_type
|
||||
|
||||
var request_body: String = ""
|
||||
if body == null || (body is String && body == ""):
|
||||
request_body = ""
|
||||
elif body is Object && body.has_method("to_json"):
|
||||
request_body = body.to_json()
|
||||
else:
|
||||
request_body = JSON.stringify(body)
|
||||
|
||||
var req: BufferedHTTPClient.RequestData = client.request(api_host + path, method, header, request_body)
|
||||
var res: BufferedHTTPClient.ResponseData = await client.wait_for_request(req)
|
||||
|
||||
# Try to fix Godot TLS Bug
|
||||
if res.result == 5:
|
||||
return await retry(req, res, path, method, body, content_type, error_count + 1)
|
||||
|
||||
match res.response_code:
|
||||
400:
|
||||
var error_message: String = res.response_data.get_string_from_utf8()
|
||||
_log.e("'%s' failed cause of: \\n%s" % [path, error_message])
|
||||
401: # Token expired / or missing permissions
|
||||
_log.e("'%s' is unauthorized. It is probably your scopes." % path)
|
||||
unauthorized.emit()
|
||||
403:
|
||||
_log.i("'%s' is unauthenticated. Refresh token." % path)
|
||||
unauthenticated.emit()
|
||||
await token.authorized
|
||||
return await retry(req, res, path, method, body, content_type, error_count + 1)
|
||||
return res
|
||||
|
||||
|
||||
func retry(request: BufferedHTTPClient.RequestData,
|
||||
response: BufferedHTTPClient.ResponseData,
|
||||
path: String,
|
||||
method: int,
|
||||
body: Variant = "",
|
||||
content_type: String = "",
|
||||
error_count: int = 0) -> BufferedHTTPClient.ResponseData:
|
||||
if error_count + 1 < MAX_AUTH_ERRORS:
|
||||
return await request(path, method, body, content_type, error_count + 1)
|
||||
else:
|
||||
# Give up the request after trying multiple times and
|
||||
# return an empty response with correct error code
|
||||
var empty_response: BufferedHTTPClient.ResponseData = client.empty_response(request)
|
||||
empty_response.response_code = response.response_code
|
||||
return empty_response
|
||||
|
||||
|
||||
## Converts unix timestamp to RFC 3339 (example: 2021-10-27T00:00:00Z) when passed a string uses as is
|
||||
static func get_rfc_3339_date_format(time: Variant) -> String:
|
||||
if typeof(time) == TYPE_INT:
|
||||
var date_time = Time.get_datetime_dict_from_unix_time(time)
|
||||
return "%s-%02d-%02dT%02d:%02d:%02dZ" % [date_time['year'], date_time['month'], date_time['day'], date_time['hour'], date_time['minute'], date_time['second']]
|
||||
return str(time)
|
||||
|
||||
"""
|
||||
|
||||
@export var parser: TwitchAPIParser
|
||||
|
||||
var grouped_files: Dictionary[String, Variant] = {}
|
||||
|
||||
|
||||
func prepare_component(component: TwitchGenComponent) -> void:
|
||||
if component._is_root:
|
||||
var base_name = get_base_name(component._classname)
|
||||
|
||||
# No suffix class lives by its own
|
||||
if base_name == component._classname:
|
||||
if grouped_files.has(base_name):
|
||||
push_error("That file shouldn't exist: %s" % base_name)
|
||||
component._classname = "Twitch" + component._classname
|
||||
grouped_files[base_name] = component
|
||||
else:
|
||||
var file: GroupedComponent = grouped_files.get(base_name, GroupedComponent.new())
|
||||
file.base_name = "Twitch" + base_name
|
||||
file.components.append(component)
|
||||
grouped_files[base_name] = file
|
||||
component._classname = component._classname.trim_prefix(base_name)
|
||||
component.set_meta("fqdn", file.base_name + "." + component._classname)
|
||||
var sub_components_to_update: Array[TwitchGenComponent] = component._sub_components.values().duplicate()
|
||||
for sub_component in sub_components_to_update:
|
||||
sub_component._classname = component._classname + sub_component._classname
|
||||
sub_components_to_update.append_array(sub_component._sub_components.values())
|
||||
pass
|
||||
|
||||
|
||||
|
||||
func generate_api() -> void:
|
||||
for component: Variant in parser.components:
|
||||
prepare_component(component)
|
||||
|
||||
# Generate TwitchAPI
|
||||
var twitch_api_code = twitch_api_header
|
||||
for method: TwitchGenMethod in parser.methods:
|
||||
twitch_api_code += method_code(method)
|
||||
write_output_file(api_output_path + "twitch_api.gd", twitch_api_code)
|
||||
|
||||
# Generate Components
|
||||
for component: Variant in grouped_files.values():
|
||||
var code = ""
|
||||
if component is GroupedComponent:
|
||||
code = group_code(component)
|
||||
else:
|
||||
code = component_code(component, 0)
|
||||
write_output_file(api_output_path + component.get_filename(), code)
|
||||
|
||||
print("API regenerated you can find it under: %s" % api_output_path)
|
||||
|
||||
|
||||
class GroupedComponent extends RefCounted:
|
||||
var base_name: String
|
||||
var prefix: String
|
||||
var components: Array[TwitchGenComponent] = []
|
||||
|
||||
|
||||
func _update_base_name(val: String) -> void:
|
||||
base_name = val
|
||||
|
||||
|
||||
func get_filename() -> String:
|
||||
return base_name.to_snake_case() + ".gd"
|
||||
|
||||
|
||||
func group_code(group: GroupedComponent) -> String:
|
||||
var code = """@tool
|
||||
extends TwitchData
|
||||
|
||||
# CLASS GOT AUTOGENERATED DON'T CHANGE MANUALLY. CHANGES CAN BE OVERWRITTEN EASILY.
|
||||
|
||||
class_name {name}
|
||||
""".format({"name": group.base_name})
|
||||
for component in group.components:
|
||||
component._is_root = false
|
||||
code += "\n\n"
|
||||
code += component_code(component, 1)
|
||||
return code
|
||||
|
||||
#region Field Code Generation
|
||||
|
||||
func field_declaration(field: TwitchGenField) -> String:
|
||||
var type = get_type(field._type, field._is_array)
|
||||
return """
|
||||
## {description}
|
||||
@export var {name}: {type}:
|
||||
set(val):
|
||||
{name} = val
|
||||
track_data(&"{name}", val)\n""".format({
|
||||
"name": field._name,
|
||||
"description": ident(field._description, 0, "## "),
|
||||
"type": type
|
||||
})
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Parameter Code Generation
|
||||
|
||||
#func get_code() -> String:
|
||||
#if _name == "broadcaster_id":
|
||||
#var default_value = "default_broadcaster_login" if _type == "String" else "[default_broadcaster_login]"
|
||||
#return "%s: %s = %s" % [_name, get_type(), default_value]
|
||||
#return "%s: %s" % [_name, get_type()]
|
||||
|
||||
#endregion
|
||||
|
||||
#region Method Code Generation
|
||||
|
||||
func parameter_doc(method: TwitchGenMethod) -> String:
|
||||
if method._required_parameters.is_empty():
|
||||
return "## [no required query parameters to describe]"
|
||||
var doc : String = ""
|
||||
for parameter: TwitchGenParameter in method._required_parameters:
|
||||
doc += "## {name} - {documentation} \n".format({
|
||||
'name': parameter._name,
|
||||
'documentation': ident(parameter._description, 0, "## ")
|
||||
})
|
||||
return doc.rstrip("\n")
|
||||
|
||||
|
||||
func parameter_array(method: TwitchGenMethod, with_type: bool = false, fully_qualified: bool = false) -> Array[String]:
|
||||
var parameters : Array[String] = []
|
||||
if method._contains_body: parameters.append(get_parameter("body", method._body_type, false, with_type, fully_qualified))
|
||||
if method._contains_optional: parameters.append(get_parameter("opt", method.get_optional_type(), false, with_type, fully_qualified))
|
||||
|
||||
method._parameters.sort_custom(TwitchGenParameter.sort)
|
||||
for parameter: TwitchGenParameter in method._required_parameters:
|
||||
parameters.append(get_parameter(parameter._name, parameter._type, parameter._is_array, with_type, fully_qualified))
|
||||
return parameters
|
||||
|
||||
|
||||
func method_parameter(method: TwitchGenMethod, with_type: bool = false, fully_qualified: bool = false) -> String:
|
||||
return ", ".join(parameter_array(method, with_type, fully_qualified))
|
||||
|
||||
|
||||
func path_code(method: TwitchGenMethod) -> String:
|
||||
var body_code : String = "var path = \"%s?\"\n" % method._path
|
||||
|
||||
if method._contains_optional:
|
||||
body_code += "var optionals: Dictionary[StringName, Variant] = {}\n"
|
||||
body_code += "if opt != null: optionals = opt.to_dict()\n"
|
||||
|
||||
for parameter: TwitchGenParameter in method._parameters:
|
||||
if parameter._required:
|
||||
body_code += parameter_path_code(parameter) + "\n"
|
||||
else:
|
||||
body_code += "if optionals.has(\"%s\"):\n" % parameter._name
|
||||
body_code += "\t%s\n" % ident(parameter_path_code(parameter, "optionals."), 1)
|
||||
return body_code
|
||||
|
||||
|
||||
func parameter_path_code(parameter: TwitchGenParameter, prefix: String = "") -> String:
|
||||
var body: String
|
||||
if parameter._is_time:
|
||||
body = "path += \"{key}=\" + get_rfc_3339_date_format({value}) + \"&\""
|
||||
|
||||
elif parameter._is_array:
|
||||
body = """
|
||||
for param in {value}:
|
||||
path += "{key}=" + str(param) + "&" """.trim_prefix("\n\t")
|
||||
else:
|
||||
body = "path += \"{key}=\" + str({value}) + \"&\""
|
||||
|
||||
return body.format({
|
||||
'value': prefix + parameter._name,
|
||||
'key': parameter._name
|
||||
})
|
||||
|
||||
## Exceptional method cause twitch api is not uniform
|
||||
func paging_code_stream_schedule() -> String:
|
||||
return """
|
||||
if parsed_result.data.pagination != null:
|
||||
opt.after = parsed_result.data.pagination.cursor
|
||||
parsed_result.data._next_page = get_channel_stream_schedule.bind(opt, broadcaster_id)\n"""
|
||||
|
||||
|
||||
func paging_code(method: TwitchGenMethod) -> String:
|
||||
if method._name == "get_channel_stream_schedule":
|
||||
return paging_code_stream_schedule()
|
||||
|
||||
var code: String = ""
|
||||
code += "if parsed_result.pagination != null:\n"
|
||||
var after_parameter: TwitchGenParameter = method.get_parameter_by_name("after")
|
||||
var result_component: TwitchGenComponent = get_component(method._result_type)
|
||||
var pagination_parameter: TwitchGenField = result_component.get_field_by_name("pagination")
|
||||
if pagination_parameter == null:
|
||||
print("Check %s paging without paging?" % method._name)
|
||||
pass
|
||||
elif pagination_parameter._type == "String":
|
||||
code += "\tvar cursor: String = parsed_result.pagination\n"
|
||||
else:
|
||||
code += "\tvar cursor: String = parsed_result.pagination.cursor\n"
|
||||
if after_parameter._required:
|
||||
code += "\t{parameter} = cursor\n"
|
||||
else:
|
||||
code += "\topt.{parameter} = cursor\n"
|
||||
code += "\tparsed_result._next_page = {name}.bind({parameters})\n"
|
||||
|
||||
return code.format({
|
||||
"parameter": after_parameter._name,
|
||||
"name": method._name,
|
||||
"parameters": method_parameter(method)
|
||||
})
|
||||
|
||||
|
||||
func response_code(method: TwitchGenMethod) -> String:
|
||||
var code: String = ""
|
||||
var result_type = get_type(method._result_type, false, true)
|
||||
if result_type != "BufferedHTTPClient.ResponseData":
|
||||
code = """
|
||||
var result: Variant = JSON.parse_string(response.response_data.get_string_from_utf8())
|
||||
var parsed_result: {result_type} = {result_type}.from_json(result)
|
||||
parsed_result.response = response
|
||||
""".format({ 'result_type': result_type })
|
||||
if method._has_paging: code += paging_code(method)
|
||||
code += "return parsed_result"
|
||||
else:
|
||||
code = "return response"
|
||||
return code
|
||||
|
||||
|
||||
func method_code(method: TwitchGenMethod) -> String:
|
||||
return """
|
||||
|
||||
## {summary}
|
||||
##
|
||||
{parameter_doc}
|
||||
##
|
||||
## {doc_url}
|
||||
func {name}({parameters}) -> {result_type}:
|
||||
{path_code}
|
||||
var response: BufferedHTTPClient.ResponseData = await request(path, HTTPClient.METHOD_{method}, {body_variable}, "{content_type}")
|
||||
{response_code}
|
||||
""".format({
|
||||
"summary": method._summary,
|
||||
"parameter_doc": parameter_doc(method),
|
||||
"doc_url": method._doc_url,
|
||||
"name": method._name,
|
||||
"parameters": method_parameter(method, true, true),
|
||||
"result_type": get_type(method._result_type, false, true),
|
||||
"path_code": ident(path_code(method), 1),
|
||||
"content_type": get_type(method._content_type, false, true),
|
||||
"method": method._http_verb.to_upper(),
|
||||
"body_variable": "body" if method._contains_body else "\"\"",
|
||||
"response_code": ident(response_code(method), 1),
|
||||
})
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Component Code Generation
|
||||
|
||||
|
||||
func component_code(component: TwitchGenComponent, level: int = 0) -> String:
|
||||
var code: String = ""
|
||||
if component._is_root:
|
||||
code += """@tool
|
||||
extends TwitchData
|
||||
|
||||
# CLASS GOT AUTOGENERATED DON'T CHANGE MANUALLY. CHANGES CAN BE OVERWRITTEN EASILY.
|
||||
|
||||
## {description}
|
||||
## {ref}
|
||||
class_name {classname}
|
||||
"""
|
||||
else:
|
||||
code = """
|
||||
## {description}
|
||||
## {ref}
|
||||
class {classname} extends TwitchData:
|
||||
"""
|
||||
var class_code : String = ""
|
||||
for field: TwitchGenField in component._fields:
|
||||
class_code += field_declaration(field)
|
||||
|
||||
if component._is_response:
|
||||
class_code += "var response: BufferedHTTPClient.ResponseData"
|
||||
class_code += "\n\n"
|
||||
class_code += create_code(component) + "\n\n"
|
||||
class_code += from_json_code(component)
|
||||
|
||||
if component._has_paging:
|
||||
class_code += "\n\n" + iter_code(component)
|
||||
|
||||
var sub_component_code: String
|
||||
for sub_component in component._sub_components.values():
|
||||
sub_component_code += "\n\n" + component_code(sub_component, 1)
|
||||
|
||||
return code.format({
|
||||
"description": ident(component._description, 0, "## "),
|
||||
"classname": component._classname,
|
||||
"ref": component._ref
|
||||
}) + ident(class_code, level) + sub_component_code
|
||||
|
||||
|
||||
func create_code(component: TwitchGenComponent) -> String:
|
||||
var parameters: Array[String] = []
|
||||
for field in component._fields:
|
||||
if field._is_required:
|
||||
parameters.append("_" + get_parameter(field._name, field._type, field._is_array))
|
||||
|
||||
var variable_name = component._classname.to_snake_case()
|
||||
var code : String = """
|
||||
## Constructor with all required fields.
|
||||
static func create({parameters}) -> {classname}:
|
||||
var {variablename}: {classname} = {classname}.new()\n""".format({
|
||||
"parameters": ", ".join(parameters),
|
||||
"classname": component._classname,
|
||||
"variablename": variable_name
|
||||
})
|
||||
|
||||
for field in component._fields:
|
||||
if field._is_required:
|
||||
code += "\t{classname}.{name} = _{name}\n".format({
|
||||
"name": field._name,
|
||||
"classname": variable_name
|
||||
})
|
||||
|
||||
code += "\treturn %s" % variable_name
|
||||
return code
|
||||
|
||||
|
||||
func from_json_code(component: TwitchGenComponent) -> String:
|
||||
var code : String = """
|
||||
static func from_json(d: Dictionary) -> {classname}:
|
||||
var result: {classname} = {classname}.new()
|
||||
""".format({"classname": component._classname})
|
||||
for field: TwitchGenField in component._fields:
|
||||
code += "\tif d.get(\"{name}\", null) != null:\n"
|
||||
if field._is_typed_array:
|
||||
code += """
|
||||
for value in d["{name}"]:
|
||||
result.{name}.append({type}.from_json(value))\n""".lstrip("\n")
|
||||
elif field._is_array:
|
||||
code += """
|
||||
for value in d["{name}"]:
|
||||
result.{name}.append(value)\n""".lstrip("\n")
|
||||
elif field._is_sub_class:
|
||||
code += "\t\tresult.{name} = {type}.from_json(d[\"{name}\"])\n"
|
||||
else:
|
||||
code += "\t\tresult.{name} = d[\"{name}\"]\n"
|
||||
code = code.format({
|
||||
"name": field._name,
|
||||
"type": get_type(field._type, false)
|
||||
})
|
||||
code += "\treturn result\n"
|
||||
|
||||
return code
|
||||
|
||||
|
||||
func iter_code(component: TwitchGenComponent) -> String:
|
||||
var data_variable_name: String = "data"
|
||||
var path_to_data: String = ""
|
||||
if component._ref == "#/components/schemas/GetChannelStreamScheduleResponse/Data":
|
||||
data_variable_name = "segments"
|
||||
path_to_data = "data."
|
||||
|
||||
var code: String
|
||||
if component._ref == "#/components/schemas/GetExtensionLiveChannelsResponse":
|
||||
code += """
|
||||
func _has_pagination() -> bool:
|
||||
if pagination == null || pagination == "": return false
|
||||
return true
|
||||
"""
|
||||
else:
|
||||
code += """
|
||||
func _has_pagination() -> bool:
|
||||
if pagination == null: return false
|
||||
if pagination.cursor == null || pagination.cursor == "": return false
|
||||
return true
|
||||
"""
|
||||
|
||||
code += """
|
||||
var _next_page: Callable
|
||||
var _cur_iter: int = 0
|
||||
|
||||
|
||||
func next_page() -> {response_type}:
|
||||
var response: {response_type} = await _next_page.call()
|
||||
_cur_iter = 0
|
||||
_next_page = response.{path_to_data}_next_page
|
||||
{copy_code}
|
||||
return response
|
||||
|
||||
|
||||
func _iter_init(iter: Array) -> bool:
|
||||
if {data_variable_name}.is_empty(): return false
|
||||
iter[0] = {data_variable_name}[0]
|
||||
return {data_variable_name}.size() > 0
|
||||
|
||||
|
||||
func _iter_next(iter: Array) -> bool:
|
||||
if {data_variable_name}.size() - 1 > _cur_iter:
|
||||
_cur_iter += 1
|
||||
iter[0] = {data_variable_name}[_cur_iter]
|
||||
if {data_variable_name}.size() - 1 == _cur_iter && not _has_pagination():
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _iter_get(iter: Variant) -> Variant:
|
||||
if {data_variable_name}.size() - 1 == _cur_iter && _has_pagination():
|
||||
await next_page()
|
||||
return iter"""
|
||||
var copy_code: String
|
||||
for field in component._fields:
|
||||
copy_code += "\t{_name} = response.{path_to_data}{_name}\n".format(field)
|
||||
|
||||
return code.format({
|
||||
"data_variable_name": data_variable_name,
|
||||
"copy_code": copy_code,
|
||||
"path_to_data": path_to_data,
|
||||
"response_type": component.get_root_classname()
|
||||
})
|
||||
#endregion
|
||||
|
||||
#region Utils
|
||||
|
||||
|
||||
func get_type(type: String, is_array: bool = false, full_qualified: bool = false) -> String:
|
||||
var result_type : String = ""
|
||||
if type.begins_with("#"):
|
||||
var component: TwitchGenComponent = parser.get_component_by_ref(type)
|
||||
result_type = component._classname
|
||||
if full_qualified and component.has_meta("fqdn"):
|
||||
result_type = component.get_meta("fqdn")
|
||||
else:
|
||||
result_type = type
|
||||
return result_type if not is_array else "Array[%s]" % result_type
|
||||
|
||||
|
||||
func get_component(type: String) -> TwitchGenComponent:
|
||||
if type.begins_with("#"):
|
||||
return parser.get_component_by_ref(type)
|
||||
else:
|
||||
return null
|
||||
|
||||
func ident(code: String, level: int, padding: String = "") -> String:
|
||||
return code.replace("\n", "\n" + "\t".repeat(level) + padding)
|
||||
|
||||
|
||||
# Writes the processed content to the output file.
|
||||
func write_output_file(file_output: String, content: String) -> void:
|
||||
var file = FileAccess.open(file_output, FileAccess.WRITE);
|
||||
if file == null:
|
||||
var error_message = error_string(FileAccess.get_open_error());
|
||||
push_error("Failed to open output file: %s\n%s" % [file_output, error_message])
|
||||
return
|
||||
file.store_string(content)
|
||||
file.flush()
|
||||
file.close()
|
||||
|
||||
|
||||
func get_base_name(file: String) -> String:
|
||||
var new_file: String = file
|
||||
for suffix: String in suffixes:
|
||||
new_file = new_file.trim_suffix(suffix)
|
||||
return new_file
|
||||
|
||||
|
||||
func get_parameter(title: String, type: String, is_array = false, with_type: bool = true, fully_qualified: bool = false) -> String:
|
||||
if with_type:
|
||||
return "{name}: {type}".format({
|
||||
"name": title,
|
||||
"type": get_type(type, is_array, fully_qualified)
|
||||
})
|
||||
else:
|
||||
return title
|
||||
|
||||
#endregion
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://cetl2un34bjb1
|
||||
247
addons/twitcher/editor/api_generator/twitch_api_parser.gd
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
@icon("res://addons/twitcher/assets/api-icon.svg")
|
||||
@tool
|
||||
extends Twitcher
|
||||
|
||||
class_name TwitchAPIParser
|
||||
|
||||
const SWAGGER_API = "https://raw.githubusercontent.com/DmitryScaletta/twitch-api-swagger/refs/heads/main/openapi.json"
|
||||
|
||||
var definition: Dictionary = {}
|
||||
var component_map: Dictionary[String, TwitchGenComponent] = {}
|
||||
var components: Array[TwitchGenComponent] = []
|
||||
var methods: Array[TwitchGenMethod] = []
|
||||
|
||||
var client: BufferedHTTPClient = BufferedHTTPClient.new()
|
||||
|
||||
signal component_added(component: TwitchGenComponent)
|
||||
signal method_added(method: TwitchGenMethod)
|
||||
|
||||
|
||||
class ComponentRepo extends RefCounted:
|
||||
var _component: TwitchGenComponent
|
||||
var _component_map: Dictionary[String, TwitchGenComponent]
|
||||
|
||||
|
||||
func get_comp(component_name: String) -> TwitchGenComponent:
|
||||
var component = _component.get_component(component_name)
|
||||
if component != null: return component
|
||||
return _component_map.get(component_name)
|
||||
|
||||
|
||||
func _init(component: TwitchGenComponent, component_map: Dictionary[String, TwitchGenComponent]) -> void:
|
||||
_component = component
|
||||
_component_map = component_map
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
client.name = "APIGeneratorClient"
|
||||
|
||||
|
||||
func parse_api() -> void:
|
||||
print("start generating API")
|
||||
if definition == {}:
|
||||
print("load Twitch definition")
|
||||
definition = await _load_swagger_definition()
|
||||
|
||||
_parsing_components()
|
||||
_parsing_paths()
|
||||
|
||||
|
||||
func _load_swagger_definition() -> Dictionary:
|
||||
add_child(client)
|
||||
client.max_error_count = 3
|
||||
var request = client.request(SWAGGER_API, HTTPClient.METHOD_GET, {}, "")
|
||||
var response_data = await client.wait_for_request(request)
|
||||
|
||||
if response_data.error:
|
||||
printerr("Cant generate API")
|
||||
return {}
|
||||
var response_str = response_data.response_data.get_string_from_utf8()
|
||||
var response = JSON.parse_string(response_str)
|
||||
remove_child(client)
|
||||
return response
|
||||
|
||||
|
||||
func _parsing_components() -> void:
|
||||
var schemas = definition["components"]["schemas"]
|
||||
for schema_name in schemas:
|
||||
var schema: Dictionary = schemas[schema_name]
|
||||
if schema["type"] != "object":
|
||||
printerr("Not an object")
|
||||
continue
|
||||
|
||||
var ref = "#/components/schemas/" + schema_name
|
||||
var component = TwitchGenComponent.new(schema_name, ref)
|
||||
component._is_root = true
|
||||
component._is_response = true
|
||||
_parse_properties(component, schema)
|
||||
_add_component(ref, component)
|
||||
|
||||
|
||||
func _parse_properties(component: TwitchGenComponent, schema: Dictionary) -> void:
|
||||
var properties = schema["properties"]
|
||||
for property_name: String in properties:
|
||||
var property: Dictionary = properties[property_name]
|
||||
var field: TwitchGenField = TwitchGenField.new()
|
||||
field._name = property_name
|
||||
field._description = property.get("description", "")
|
||||
field._type = _get_param_type(property)
|
||||
|
||||
var classname: String = property_name.capitalize().replace(" ", "")
|
||||
|
||||
|
||||
if property.has("properties"):
|
||||
var sub_component = _add_sub_component(classname, field._description, component, property)
|
||||
field._type = sub_component._ref
|
||||
|
||||
## Arrays that has custom types
|
||||
elif property.get("type", "") == "array":
|
||||
field._is_array = true
|
||||
field._is_sub_class = false
|
||||
var items = property.get("items", {})
|
||||
if items.has("$ref"):
|
||||
field._type = items.get("$ref")
|
||||
elif items.has("properties"):
|
||||
var sub_component = _add_sub_component(classname, field._description, component, items)
|
||||
field._type = sub_component._ref
|
||||
|
||||
component.add_field(field)
|
||||
|
||||
var requires: Array = schema.get("required", [])
|
||||
for required_field: String in requires:
|
||||
var field: TwitchGenField = component.get_field_by_name(required_field)
|
||||
field._is_required = true
|
||||
|
||||
|
||||
func _add_sub_component(classname: String, description: String, parent_component: TwitchGenComponent, properties: Dictionary) -> TwitchGenComponent:
|
||||
var ref: String = parent_component._ref + "/" + classname
|
||||
var sub_component = TwitchGenComponent.new(classname, ref)
|
||||
sub_component._description = description
|
||||
_parse_properties(sub_component, properties)
|
||||
parent_component.add_component(sub_component)
|
||||
_add_component(ref, sub_component)
|
||||
return sub_component
|
||||
|
||||
|
||||
func _parsing_paths() -> void:
|
||||
var paths = definition.get("paths", {})
|
||||
for path in paths:
|
||||
var method_specs = paths[path]
|
||||
for http_verb: String in method_specs:
|
||||
var method_spec = method_specs[http_verb] as Dictionary
|
||||
var method = _parse_method(http_verb, method_spec)
|
||||
method._path = path
|
||||
if method._contains_optional:
|
||||
var component : TwitchGenComponent = method.get_optional_component()
|
||||
_add_component(component._ref, component)
|
||||
methods.append(method)
|
||||
method_added.emit(method)
|
||||
|
||||
|
||||
func _parse_method(http_verb: String, method_spec: Dictionary) -> TwitchGenMethod:
|
||||
var method: TwitchGenMethod = TwitchGenMethod.new()
|
||||
method._http_verb = http_verb
|
||||
method._name = method_spec.get("operationId", "method_" + http_verb).replace("-", "_")
|
||||
method._summary = method_spec.get("summary", "No summary provided.")
|
||||
method._description = method_spec.get("description", "No description provided.")
|
||||
method._doc_url = method_spec.get("externalDocs", {}).get("url", "No link provided")
|
||||
_parse_parameters(method, method_spec)
|
||||
|
||||
# Body Type
|
||||
if method_spec.has("requestBody"):
|
||||
method._body_type = "Dictionary"
|
||||
var ref = method_spec.get("requestBody").get("content", {}).get("application/json", {}).get("schema", {}).get("$ref", "")
|
||||
if ref != "":
|
||||
method._body_type = ref
|
||||
|
||||
# Result Type
|
||||
method._result_type = "BufferedHTTPClient.ResponseData"
|
||||
var responses = method_spec.get("responses", {})
|
||||
if responses.has("200") || responses.has("202"):
|
||||
var content: Dictionary = {}
|
||||
if responses.has("200"):
|
||||
content = responses["200"].get("content", {})
|
||||
elif content == {}:
|
||||
content = responses["202"].get("content", {})
|
||||
|
||||
# Assuming the successful response is a JSON object
|
||||
method._result_type = "Dictionary"
|
||||
|
||||
# Special case for /schedule/icalendar
|
||||
if content.has("text/calendar"):
|
||||
method._result_type = "BufferedHTTPClient.ResponseData"
|
||||
|
||||
|
||||
# Try to resolve the component references
|
||||
var ref = content.get("application/json", {}).get("schema", {}).get("$ref", "")
|
||||
if ref != "":
|
||||
method._result_type = ref
|
||||
|
||||
# Content Type
|
||||
if method_spec.has("requestBody"):
|
||||
var requestBody = method_spec.get("requestBody")
|
||||
var content = requestBody.get("content")
|
||||
method._content_type = content.keys()[0]
|
||||
elif http_verb == "POST":
|
||||
method._content_type = "application/x-www-form-urlencoded"
|
||||
return method
|
||||
|
||||
|
||||
func _parse_parameters(method: TwitchGenMethod, method_spec: Dictionary) -> void:
|
||||
var parameter_specs = method_spec.get("parameters", [])
|
||||
for parameter_spec in parameter_specs:
|
||||
var parameter: TwitchGenParameter = TwitchGenParameter.new()
|
||||
var schema = parameter_spec["schema"]
|
||||
parameter._name = parameter_spec.get("name", "")
|
||||
parameter._description = parameter_spec.get("description", "")
|
||||
parameter._type = _get_param_type(schema)
|
||||
parameter._required = parameter_spec.get("required", false)
|
||||
parameter._is_time = schema.get("format", "") == "date-time"
|
||||
parameter._is_array = schema.get("type", "") == "array"
|
||||
method.add_parameter(parameter)
|
||||
|
||||
|
||||
func _add_component(ref: String, component: TwitchGenComponent) -> void:
|
||||
components.append(component)
|
||||
component_map[ref] = component
|
||||
|
||||
|
||||
func get_component_by_ref(ref: String) -> TwitchGenComponent:
|
||||
return component_map[ref]
|
||||
|
||||
|
||||
func _get_param_type(schema: Dictionary) -> String:
|
||||
if schema.has("$ref"):
|
||||
return schema["$ref"]
|
||||
|
||||
if not schema.has("type"):
|
||||
return "Variant" # Maybe ugly
|
||||
|
||||
var type = schema["type"]
|
||||
var format = schema.get("format", "")
|
||||
match type:
|
||||
"object":
|
||||
if schema.has("additinalProperties"):
|
||||
return _get_param_type(schema["additinalProperties"])
|
||||
return "Dictionary"
|
||||
"string":
|
||||
# Why did I do this in the first place?
|
||||
# Lets disable and see if problems appear
|
||||
#if format == "date-time":
|
||||
# return "Variant"
|
||||
return "String"
|
||||
"integer":
|
||||
return "int"
|
||||
"number":
|
||||
return "float" if format == "float" else "int"
|
||||
"boolean":
|
||||
return "bool"
|
||||
"array":
|
||||
var ref: String = schema["items"].get("$ref", "")
|
||||
if schema["items"].get("type", "") == "string":
|
||||
return "String"
|
||||
elif ref != "":
|
||||
return ref
|
||||
else:
|
||||
return "Variant"
|
||||
_: return "Variant"
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://o7q04krfh33l
|
||||
3
addons/twitcher/editor/api_generator/twitch_gen.gd
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
extends RefCounted
|
||||
|
||||
class_name TwitchGen
|
||||
1
addons/twitcher/editor/api_generator/twitch_gen.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cvtstbwwwf7gf
|
||||
60
addons/twitcher/editor/api_generator/twitch_gen_component.gd
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
@tool
|
||||
extends TwitchGen
|
||||
|
||||
class_name TwitchGenComponent
|
||||
|
||||
var _classname: String
|
||||
var _ref: String
|
||||
var _description: String
|
||||
var _fields: Array[TwitchGenField] = []
|
||||
var _field_map: Dictionary[String, TwitchGenField] = {}
|
||||
var _parent_component: TwitchGenComponent
|
||||
var _sub_components: Dictionary[String, TwitchGenComponent] = {}
|
||||
var _is_root: bool
|
||||
var _is_response: bool
|
||||
var _has_paging: bool
|
||||
var _filename: String:
|
||||
get(): return _classname.to_snake_case() + ".gd"
|
||||
|
||||
|
||||
func _init(classname: String, ref: String) -> void:
|
||||
_classname = _sanatize_classname(classname)
|
||||
_ref = ref
|
||||
|
||||
|
||||
func add_field(field: TwitchGenField) -> void:
|
||||
_fields.append(field)
|
||||
_field_map[field._name] = field
|
||||
if field._name == "pagination":
|
||||
_has_paging = true
|
||||
|
||||
|
||||
func get_field_by_name(field_name: String) -> TwitchGenField:
|
||||
return _field_map.get(field_name, null)
|
||||
|
||||
|
||||
func get_root_classname() -> String:
|
||||
var parent: TwitchGenComponent = self
|
||||
while parent._parent_component != null:
|
||||
parent = parent._parent_component
|
||||
return parent._classname
|
||||
|
||||
|
||||
func get_filename() -> String:
|
||||
return get_root_classname().to_snake_case() + ".gd"
|
||||
|
||||
|
||||
func _sanatize_classname(val: String) -> String:
|
||||
match val:
|
||||
"Image": return "TwitchImage"
|
||||
"Panel": return "TwitchPanel"
|
||||
_: return val
|
||||
|
||||
|
||||
func get_component(component_name: String) -> TwitchGenComponent:
|
||||
return _sub_components.get(component_name)
|
||||
|
||||
|
||||
func add_component(sub_component: TwitchGenComponent) -> void:
|
||||
_sub_components[sub_component._classname] = sub_component
|
||||
sub_component._parent_component = self
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://cttaojg743q5y
|
||||
31
addons/twitcher/editor/api_generator/twitch_gen_field.gd
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
@tool
|
||||
extends TwitchGen
|
||||
|
||||
class_name TwitchGenField
|
||||
|
||||
var _name: String:
|
||||
set = _update_name
|
||||
var _description: String
|
||||
var _type: String
|
||||
var _is_required: bool
|
||||
var _is_sub_class: bool:
|
||||
get(): return _type.begins_with("#")
|
||||
var _is_array: bool
|
||||
var _is_typed_array: bool:
|
||||
get(): return _is_array && _type.begins_with("#")
|
||||
|
||||
|
||||
## Couple of names from the Twitch API are messed up like keywords for godot or numbers
|
||||
func _update_name(val: String) -> void:
|
||||
match val:
|
||||
"animated": _name = "animated_format"
|
||||
"static": _name = "static_format"
|
||||
"1": _name = "_1"
|
||||
"2": _name = "_2"
|
||||
"3": _name = "_3"
|
||||
"4": _name = "_4"
|
||||
"1.5": _name = "_1_5"
|
||||
"100x100": _name = "_100x100"
|
||||
"24x24": _name = "_24x24"
|
||||
"300x200": _name = "_300x200"
|
||||
_: _name = val
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://dgx1w74garjdh
|
||||
59
addons/twitcher/editor/api_generator/twitch_gen_method.gd
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
@tool
|
||||
extends TwitchGen
|
||||
|
||||
class_name TwitchGenMethod
|
||||
var _http_verb: String
|
||||
var _name: String
|
||||
var _summary: String
|
||||
var _description: String
|
||||
var _path: String
|
||||
var _doc_url: String
|
||||
var _parameters: Array[TwitchGenParameter] = []
|
||||
var _parameter_map: Dictionary[String, TwitchGenParameter] = {}
|
||||
var _required_parameters: Array[TwitchGenParameter]:
|
||||
get(): return _parameters.filter(func(p): return p._required)
|
||||
var _optional_parameters: Array[TwitchGenParameter]:
|
||||
get(): return _parameters.filter(func(p): return not p._required)
|
||||
var _body_type: String
|
||||
var _result_type: String
|
||||
var _content_type: String
|
||||
var _has_paging: bool
|
||||
var _contains_optional: bool
|
||||
var _contains_body: bool:
|
||||
get(): return _body_type != null and _body_type != ""
|
||||
|
||||
|
||||
func add_parameter(parameter: TwitchGenParameter) -> void:
|
||||
_parameters.append(parameter)
|
||||
_contains_optional = _contains_optional || not parameter._required
|
||||
_parameter_map[parameter._name] = parameter
|
||||
if parameter._name == "after":
|
||||
_has_paging = true
|
||||
|
||||
|
||||
func get_parameter_by_name(name: String) -> TwitchGenParameter:
|
||||
return _parameter_map.get(name)
|
||||
|
||||
|
||||
func get_optional_classname() -> String:
|
||||
return _name.capitalize().replace(" ", "") + "Opt"
|
||||
|
||||
|
||||
func get_optional_type() -> String:
|
||||
return "#/components/schemas/" + get_optional_classname()
|
||||
|
||||
|
||||
func get_optional_component() -> TwitchGenComponent:
|
||||
var component = TwitchGenComponent.new(get_optional_classname(), get_optional_type())
|
||||
component._description = "All optional parameters for TwitchAPI.%s" % _name
|
||||
component._is_root = true
|
||||
for parameter: TwitchGenParameter in _optional_parameters:
|
||||
var field = TwitchGenField.new()
|
||||
field._name = parameter._name
|
||||
field._type = parameter._type
|
||||
field._description = parameter._description
|
||||
field._is_required = false
|
||||
field._is_array = parameter._is_array
|
||||
component.add_field(field)
|
||||
|
||||
return component
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://dkwhf6s838d20
|
||||
22
addons/twitcher/editor/api_generator/twitch_gen_parameter.gd
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
@tool
|
||||
extends TwitchGen
|
||||
|
||||
class_name TwitchGenParameter
|
||||
var _name: String
|
||||
var _description: String
|
||||
var _required: bool
|
||||
var _type: String
|
||||
var _is_time: bool
|
||||
var _is_array: bool
|
||||
|
||||
static func sort(p1: TwitchGenParameter, p2: TwitchGenParameter) -> bool:
|
||||
if p1._name == "broadcaster_id":
|
||||
return false
|
||||
if p2._name == "broadcaster_id":
|
||||
return true
|
||||
if p1._required && not p2._required:
|
||||
return true
|
||||
if not p1._required && p2._required:
|
||||
return false
|
||||
return p1._name < p2._name
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://1ptmbsygcfyt
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
extends EditorInspectorPlugin
|
||||
|
||||
const EventsubConfigProperty = preload("res://addons/twitcher/editor/inspector/twitch_eventsub_config_property.gd")
|
||||
|
||||
func _can_handle(object: Object) -> bool:
|
||||
return object is TwitchEventsubConfig
|
||||
|
||||
|
||||
func _parse_property(object: Object, type: Variant.Type, name: String, \
|
||||
hint_type: PropertyHint, hint_string: String, usage_flags: int, \
|
||||
wide: bool) -> bool:
|
||||
|
||||
if name == &"condition":
|
||||
add_property_editor("condition", EventsubConfigProperty.new(), true)
|
||||
return true
|
||||
if name == &"type":
|
||||
add_property_editor("type", ToDocs.new(), true, "Documentation")
|
||||
return false
|
||||
|
||||
|
||||
class ToDocs extends EditorProperty:
|
||||
const EXT_LINK = preload("res://addons/twitcher/assets/ext-link.svg")
|
||||
|
||||
var docs = Button.new()
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
docs.text = "To dev.twitch.tv"
|
||||
docs.icon = EXT_LINK
|
||||
docs.pressed.connect(_on_to_docs)
|
||||
add_child(docs)
|
||||
add_focusable(docs)
|
||||
|
||||
|
||||
func _on_to_docs() -> void:
|
||||
var eventsub_config: TwitchEventsubConfig = get_edited_object()
|
||||
OS.shell_open(eventsub_config.definition.documentation_link)
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://c1afm3xjonwxr
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
extends EditorProperty
|
||||
|
||||
const USER_CONVERTER = preload("res://addons/twitcher/editor/inspector/user_converter.tscn")
|
||||
const TwitchEditorSettings = preload("res://addons/twitcher/editor/twitch_editor_settings.gd")
|
||||
|
||||
var _container: Node = GridContainer.new()
|
||||
|
||||
func _init():
|
||||
_container.columns = 2
|
||||
add_child(_container)
|
||||
set_bottom_editor(_container)
|
||||
|
||||
|
||||
func _on_type_change(new_type: TwitchEventsubDefinition.Type) -> void:
|
||||
var eventsub_config: TwitchEventsubConfig = get_edited_object();
|
||||
if eventsub_config != null:
|
||||
for meta in eventsub_config.get_meta_list():
|
||||
if meta.ends_with("_user"):
|
||||
eventsub_config.remove_meta(meta)
|
||||
_create_conditions()
|
||||
|
||||
|
||||
func _update_property() -> void:
|
||||
_create_conditions()
|
||||
|
||||
|
||||
func _create_conditions() -> void:
|
||||
for node in _container.get_children():
|
||||
node.queue_free()
|
||||
|
||||
var eventsub_config: TwitchEventsubConfig = get_edited_object();
|
||||
if eventsub_config == null || eventsub_config.get_class() == &"EditorDebuggerRemoteObject": return
|
||||
|
||||
for condition_name: StringName in eventsub_config.definition.conditions:
|
||||
var condition_value = eventsub_config.condition.get_or_add(condition_name, "")
|
||||
var condition_title = Label.new()
|
||||
condition_title.text = condition_name.capitalize()
|
||||
_container.add_child(condition_title)
|
||||
var editor_token = TwitchEditorSettings.editor_oauth_token
|
||||
if condition_name.to_lower().ends_with("user_id") && editor_token.is_token_valid():
|
||||
var user_converter = USER_CONVERTER.instantiate()
|
||||
user_converter.changed.connect(_on_changed_user.bind(condition_name))
|
||||
_container.add_child(user_converter)
|
||||
if eventsub_config.has_meta(condition_name + "_user"):
|
||||
var user = eventsub_config.get_meta(condition_name + "_user")
|
||||
user_converter.update_user(user)
|
||||
elif condition_value != "":
|
||||
user_converter.user_id = condition_value
|
||||
user_converter.reload()
|
||||
else:
|
||||
var input = LineEdit.new()
|
||||
input.text_submitted.connect(_on_change_text.bind(condition_name, input))
|
||||
input.focus_exited.connect(_on_change_text.bind("", condition_name, input))
|
||||
input.text = condition_value
|
||||
input.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_container.add_child(input)
|
||||
|
||||
|
||||
func _on_changed_user(user: TwitchUser, condition_name: StringName) -> void:
|
||||
var eventsub_config: TwitchEventsubConfig = get_edited_object();
|
||||
if user == null:
|
||||
eventsub_config.condition[condition_name] = ""
|
||||
eventsub_config.remove_meta(condition_name + "_user")
|
||||
emit_changed(&"condition", eventsub_config.condition)
|
||||
else:
|
||||
eventsub_config.condition[condition_name] = user.id
|
||||
eventsub_config.set_meta(condition_name + "_user", user)
|
||||
emit_changed(&"condition", eventsub_config.condition)
|
||||
|
||||
|
||||
func _on_change_text(new_text: String, condition_name: StringName, input: LineEdit) -> void:
|
||||
print("BLUB")
|
||||
var eventsub_config: TwitchEventsubConfig = get_edited_object();
|
||||
eventsub_config.condition[condition_name] = input.text
|
||||
#emit_changed(&"condition", eventsub_config.condition)
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://drv4gmn8akxf2
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
extends EditorInspectorPlugin
|
||||
|
||||
|
||||
func _can_handle(object: Object) -> bool:
|
||||
return object is TwitchEventsub || object is TwitchService
|
||||
|
||||
|
||||
func _parse_property(object: Object, type: Variant.Type, name: String, hint_type: PropertyHint, hint_string: String, usage_flags: int, wide: bool) -> bool:
|
||||
if name == &"scopes" && object.get_class() != &"EditorDebuggerRemoteObject":
|
||||
if (object is TwitchService && object.eventsub != null) || object is TwitchEventsub:
|
||||
add_property_editor(&"scope_validation", ScopeValidation.new(), true, "Scope Validation")
|
||||
return false
|
||||
|
||||
|
||||
class ScopeValidation extends EditorProperty:
|
||||
const WARNING_LABEL_SETTINGS = preload("res://addons/twitcher/assets/warning_label_settings.tres")
|
||||
const INFO_LABEL_SETTINGS = preload("res://addons/twitcher/assets/info_label_settings.tres")
|
||||
var _warning_label: Label = Label.new();
|
||||
var _apply_scopes: Button = Button.new();
|
||||
var _needed_scopes: Dictionary = {}
|
||||
var container: Control = VBoxContainer.new()
|
||||
|
||||
|
||||
func _init():
|
||||
_warning_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
_warning_label.text = "Press validate to check if scopes maybe are missing."
|
||||
|
||||
var validate_button = Button.new();
|
||||
validate_button.text = "Validate";
|
||||
validate_button.tooltip_text = "Checks the scopes of the subscriptions " \
|
||||
+ " if they match the defined scopes in the scope " \
|
||||
+ " property";
|
||||
validate_button.pressed.connect(_on_validate_scopes);
|
||||
|
||||
_apply_scopes.text = "Apply Scopes";
|
||||
_apply_scopes.tooltip_text = "Apply Scopes to the scope resource in " \
|
||||
+ " this TwitchEventsub. It maybe not needed depending on the " \
|
||||
+ " Subscription. Please check the documentation if there is a logical " \
|
||||
+ " condition and apply the scopes accordingly.";
|
||||
_apply_scopes.pressed.connect(_on_apply_scopes);
|
||||
|
||||
add_child(validate_button)
|
||||
container.add_child(_warning_label)
|
||||
add_child(container)
|
||||
set_bottom_editor(container)
|
||||
|
||||
|
||||
func _on_apply_scopes() -> void:
|
||||
var scopes = get_edited_object().scopes;
|
||||
var scopes_to_add: Array[StringName] = [];
|
||||
for scope in _needed_scopes.values():
|
||||
scopes_to_add.append(scope);
|
||||
scopes.add_scopes(scopes_to_add);
|
||||
_clear_warning();
|
||||
|
||||
|
||||
func _on_validate_scopes() -> void:
|
||||
var scopes = get_edited_object().scopes;
|
||||
var subscriptions = get_edited_object().get_subscriptions();
|
||||
|
||||
_needed_scopes.clear()
|
||||
for subscription: TwitchEventsubConfig in subscriptions:
|
||||
if subscription == null: continue
|
||||
for scope in subscription.definition.scopes:
|
||||
_needed_scopes[scope] = scope
|
||||
|
||||
for scope in scopes.used_scopes:
|
||||
_needed_scopes.erase(scope)
|
||||
|
||||
if !_needed_scopes.is_empty():
|
||||
if _apply_scopes.get_parent() == null: container.add_child(_apply_scopes)
|
||||
_warning_label.label_settings = WARNING_LABEL_SETTINGS
|
||||
var needed_scopes = ", ".join(_needed_scopes.values())
|
||||
_warning_label.text = "You may miss scopes please check documentation if you need to add: %s" % needed_scopes;
|
||||
else:
|
||||
_clear_warning()
|
||||
|
||||
|
||||
func _clear_warning() -> void:
|
||||
_warning_label.text = "Scopes seems to be OK for this EventSub."
|
||||
_warning_label.label_settings = INFO_LABEL_SETTINGS
|
||||
if _apply_scopes.get_parent() != null:
|
||||
container.remove_child(_apply_scopes)
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://cuo2g5oib1cf3
|
||||