nicklambourne / slackblocks Goto Github PK
View Code? Open in Web Editor NEW:game_die: Python API for Building Messages Using the Slack Block Kit API
License: MIT License
:game_die: Python API for Building Messages Using the Slack Block Kit API
License: MIT License
Thoughts on adding support for https://api.slack.com/reference/block-kit/blocks#rich_text to slackblocks? Happy to cut a PR but would need a little help understanding the best way to implement it given its nested object nature.
Currently on the homepage of the docs: https://nicklambourne.github.io/slackblocks/latest/ under the section "Messages" the hyperlink on the text "Messages" currently goes to a dead end of "https://nicklambourne.github.io/reference/messages/"
This should be updated to "https://nicklambourne.github.io/slackblocks/latest/reference/messages/"
There are many components like this such as:
Messages: https://nicklambourne.github.io/reference/messages/
Blocks: https://nicklambourne.github.io/reference/blocks
Elements: https://nicklambourne.github.io/reference/elements
Objects: https://nicklambourne.github.io/reference/objects
Text: https://nicklambourne.github.io/reference/objects/#objects.Text
There are more, but they all just need to have the {{version}}
added, typically latest
I'm having code like this
from slackblocks import Message, SectionBlock, ContextBlock, Text, DividerBlock, TextType
foo = SectionBlock("Test:", fields=[Text(text='foo')])
message = Message(channel="#general", blocks=foo)
message.json()
But I'm getting the following:
❯ python test.py ─╯
Traceback (most recent call last):
File "test.py", line 5, in <module>
message.json()
File "/Users/confiq/work/infra-iac/venv/lib/python3.7/site-packages/slackblocks/messages.py", line 46, in json
return dumps(self._resolve(), indent=4)
File "/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/json/__init__.py", line 238, in dumps
**kw).encode(obj)
File "/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/json/encoder.py", line 201, in encode
chunks = list(chunks)
File "/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/json/encoder.py", line 431, in _iterencode
yield from _iterencode_dict(o, _current_indent_level)
File "/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/json/encoder.py", line 405, in _iterencode_dict
yield from chunks
File "/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/json/encoder.py", line 325, in _iterencode_list
yield from chunks
File "/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/json/encoder.py", line 405, in _iterencode_dict
yield from chunks
File "/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/json/encoder.py", line 325, in _iterencode_list
yield from chunks
File "/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/json/encoder.py", line 438, in _iterencode
o = _default(o)
File "/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/json/encoder.py", line 179, in default
raise TypeError(f'Object of type {o.__class__.__name__} '
TypeError: Object of type Text is not JSON serializable
It seems it's a bug so I wanted to confirm here if it's a bug or I misread the library.
This is the json that I expect to render: api-slack-block-builder
❯ python --version
Python 3.7.7
❯ pip freeze | grep slack
slackblocks==0.1.5
When responding to interactions, it's necessary for the API server which receives the interaction to construct an acknowledgement response. The response payload can be a message, formatted in the usual way (i.e. {"blocks": [..]}
, but this message doesn't require a channel
property and can also include a replace_original: true
attribute to replace the message which was interacted with.
Proposal is to make channel
optional, or create a base class BaseMessage
which is subclassed for regular messages and acknowledgement responses.
Problem:
When trying to resolve an ExternalSelectMenu inside of an Input block the initial_option accepts Option | OptionGroup, but does not serialize it to JSON before sending to slack.
Workaround:
-When creating the blocks pre-resolving the initial option via ._resolve()
Does not work:
try:
return InputBlock(
label="example label",
optional=True,
block_id="deployable_apps_tag_select",
element=ExternalSelectMenu(
action_id="deployable_apps_select_tag",
placeholder="example placeholder",
min_query_length=0,
initial_option=Option(text=Text(text="test_text", type_=TextType.PLAINTEXT), value="test_value")
)
)
except Exception as e:
logger.warning(f"Failed to create tag select block: {e})")
raise e
Workaround:
try:
return InputBlock(
label="example label",
optional=True,
block_id="deployable_apps_tag_select",
element=ExternalSelectMenu(
action_id="deployable_apps_select_tag",
placeholder="example placeholder",
min_query_length=0,
initial_option=Option(text=Text(text="test_text", type_=TextType.PLAINTEXT), value="test_value")._resolve()
)
)
except Exception as e:
logger.warning(f"Failed to create tag select block: {e})")
raise e
Possible solution:
Updating the _resolve()
function here to automatically resolve the initial option
slackblocks/slackblocks/elements.py
Line 1272 in 843cc7a
Caveats and Questions:
-Being that this is not a MultiExternalSelectMenu
, it may not accept multiple options as an initial value and the schema should be checked to see if the constructor for ExternalSelectMenu
should only accept Option
as opposed to Option | OptionGroup
-This issue seems to be isolated to just the ExternalSelectMenu as it is working correctly for MultiExternalSelectMenu
slackblocks/slackblocks/elements.py
Line 640 in 843cc7a
Problem: DateTimePicker is now allowed inside of InputBlock
Testing:
Code:
InputBlock(label="Select Branch", element=DateTimePicker(action_id="datetimepicker-action",initial_datetime = 1628633820))
Error:
print(InputBlock(label="Select Branch", element=DateTimePicker(action_id="datetimepicker-action",initial_datetime = 1628633820)))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/lib/python3.11/site-packages/slackblocks/blocks.py", line 258, in __init__
raise InvalidUsageError("")
slackblocks.errors.InvalidUsageError
Documentation: LINK
I tested this this in a slack block editor to make sure it works inside a InputBlock
Caveats: This is only available on Desktop and only in Modal or Messages
Possible Resolution: Add DateTimePicker to the constant HERE
UserSelectMenu requires only one positional argument: 'action_id' but when:
slackblocks.UserSelectMenu('action')
then
self.placeholder = Text.to_text(
^^^^^^^^^^^^^
raise InvalidUsageError("This field cannot have the value None or ''")
slackblocks.errors.InvalidUsageError: This field cannot have the value None or ''
I need to pass null value for text=None, but I am getting error that empty or null string are not allowed.
But in the code it is optional
SectionBlock( fields=[Text(text="version", type_=TextType.PLAINTEXT))
def init(
self,
text: Optional[TextLike] = None,
block_id: Optional[str] = None,
fields: Optional[List[Text]] = None,
accessory: Optional[Element] = None,
):
Version installed 0.9.6
Dear @nicklambourne
I have tried
SectionBlock(text="For Test Button",accessory=Button(action_id="1", text="Choose"))
But got error
The server responded with: {'ok': False, 'error': 'invalid_blocks', 'response_metadata': {'messages': ['[ERROR] must be a valid enum value ...
It worked fine without accessory
SectionBlock(text="For Test Button")
Would you please guide me how to generate button?
Thank you so much.
multi_select_user = UserMultiSelectMenu(
action_id="multi_users_select",
placeholder=Text("Select one or more users", type_=TextType.PLAINTEXT),
initial_users=["U064B5H1309", "U063JR973UP"],
)
Given the above piece of code, looks like the initial_users fails to render with the below error
user_multi_select["initial_users"] = [
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
.0 = <list_iterator object at 0x7fdaa37a6b00>
user_multi_select["initial_users"] = [
> initial_option._resolve() for initial_option in self.initial_users
]
E AttributeError: 'str' object has no attribute '_resolve'
This looks to be a case of the initial_users being specified as a list of strings, but we try to call the _resolve method from all the elements in intial_users, which will of course fail.
I am happy to push a fix for this.
File "/Users/nikhilxavier/Documents/sa-backend/services/slackblock_services.py", line 214, in basic_message_modal
view = ModalView(
^^^^^^^^^^
File "/Users/nikhilxavier/Documents/sa-backend/venv/lib/python3.11/site-packages/slackblocks/views.py", line 80, in __init__
self.submit = Text.to_text(submit, force_plaintext=True, max_length=24)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/nikhilxavier/Documents/sa-backend/venv/lib/python3.11/site-packages/slackblocks/objects.py", line 114, in to_text
raise InvalidUsageError("This field cannot have the value None or ''")
slackblocks.errors.InvalidUsageError: This field cannot have the value None or ''
Tried using the modal view to create a basic modal popup with just a close button, but could not do so since modalview is expecting submit field by default although its default is None. I.e
basically converting to text is causing an issue since allow_none is set to false by default!
I feel like the alt_text
argument should be made optional, with a default value being " "
, the same way it's done in ImageBlock
.
If this change will be welcome, I'd like to make a pull-request implementing it.
Problem:
When using the default TextType created when using the Option constructor inside or appended to a StaticMultiSelectMenu or StaticSelectMenu a schema error is returned from slack due to these not supporting the markdown TextType.
Based on the documentation there seems to be a supported and unsupported category.
Supporting Slack documentation:
"Overflow, select, and multi-select menus can only use plain_text objects, while radio buttons and checkboxes can use mrkdwn text objects." - LINK
Testing:
Working:
modal = ModalView(
title="Example_Modal",
submit="Submit",
close="Cancel",
callback_id="example_callback_id",
blocks=[
SectionBlock(text="Select a branch to deploy"),
InputBlock(
label="Branch",
element=StaticSelectMenu(
placeholder="Select a branch",
action_id="branch_select",
options=[
Option(text=Text(text="branch-a",type_=TextType.PLAINTEXT), value="branch-a"),
Option(text=Text(text="branch-b",type_=TextType.PLAINTEXT), value="branch-b"),
Option(text=Text(text="branch-c",type_=TextType.PLAINTEXT), value="branch-c"),
],
),
),
SectionBlock(text="Select the environment(s) to deploy to"),
InputBlock(
label="Environments",
element=StaticMultiSelectMenu(
placeholder="Select environments",
action_id="deploy_to_env",
options=[
Option(text=Text(text="dev",type_=TextType.PLAINTEXT), value="dev"),
Option(text=Text(text="preprod",type_=TextType.PLAINTEXT), value="preprod"),
Option(text=Text(text="prod",type_=TextType.PLAINTEXT), value="prod"),
],
),
),
],
)
Not working:
modal = ModalView(
title="Example_Modal",
submit="Submit",
close="Cancel",
callback_id="example_callback_id",
blocks=[
SectionBlock(text="Select a branch to deploy"),
InputBlock(
label="Branch",
element=StaticSelectMenu(
placeholder="Select a branch",
action_id="branch_select",
options=[
Option(text="branch-a", value="branch-a"),
Option(text="branch-b", value="branch-b"),
Option(text="branch-c", value="branch-c"),
],
),
),
SectionBlock(text="Select the environment(s) to deploy to"),
InputBlock(
label="Environments",
element=StaticMultiSelectMenu(
placeholder="Select environments",
action_id="deploy_to_env",
options=[
Option(text="dev", value="dev"),
Option(text="preprod", value="preprod"),
Option(text="prod", value="prod"),
],
),
),
],
)
Error:
Error opening modal: The request to the Slack API failed. (url: https://www.slack.com/api/views.open)
The server responded with: {'ok': False, 'error': 'invalid_arguments', 'response_metadata': {'messages': ['[ERROR] failed to match all allowed schemas [json-pointer:/view]', '[ERROR] failed to match all allowed schemas [json-pointer:/view/blocks/1/element/options/0/text]', '[ERROR] must be a valid enum value [json-pointer:/view/blocks/1/element/options/0/text/type]', '[ERROR] failed to match all allowed schemas [json-pointer:/view/blocks/1/element/options/1/text]', '[ERROR] must be a valid enum value [json-pointer:/view/blocks/1/element/options/1/text/type]', '[ERROR] failed to match all allowed schemas [json-pointer:/view/blocks/1/element/options/2/text]', '[ERROR] must be a valid enum value [json-pointer:/view/blocks/1/element/options/2/text/type]', '[ERROR] failed to match all allowed schemas [json-pointer:/view/blocks/3/element/options/0/text]', '[ERROR] must be a valid enum value [json-pointer:/view/blocks/3/element/options/0/text/type]', '[ERROR] failed to match all allowed schemas [json-pointer:/view/blocks/3/element/options/1/text]', '[ERROR] must be a valid enum value [json-pointer:/view/blocks/3/element/options/1/text/type]', '[ERROR] failed to match all allowed schemas [json-pointer:/view/blocks/3/element/options/2/text]', '[ERROR] must be a valid enum value [json-pointer:/view/blocks/3/element/options/2/text/type]']}}
Possible Resolution:
-Add a check to the constructors to error if the TextType of any of the options is mrkdwn
Slack's official web client requires a dict[Unknown, Unknown]
when publishing views, and would not work unless I am calling the private _resolve()
method when using HomeTabView
.
v = HomeTabView(...)
client.views_publish(..., view=v._resolve())
I have noticed that you have added a to_dict()
method in #17 so I am wondering if it's okay to do the same for Views.
If that is okay with you, I'll be happy to submit a small PR to add it for Views.
Problem:
DatePicker errors when calling _resolve is the optional variable initial_date
is not given
Testing(This is nested inside a ModalView that renders perfectly when it is initialized with initial_date
is provided):
I am just going to provide the part of the code that stands out
delete_after_block = InputBlock(
label=Text("Delete After",type_=TextType.PLAINTEXT, emoji=True),
element=DatePicker(
action_id="datepicker-action",
#set to current date plus 4 days as unix timestamp in second
#initial_date = str(datetime.now().date() + timedelta(days=4)),
),
block_id="delete_after_block"
)
Error:
Failed to run listener function (error: 'DatePicker' object has no attribute 'initial_date')
Traceback (most recent call last):
File "/opt/homebrew/lib/python3.11/site-packages/slack_bolt/listener/thread_runner.py", line 120, in run_ack_function_asynchronously
listener.run_ack_function(request=request, response=response)
File "/opt/homebrew/lib/python3.11/site-packages/slack_bolt/listener/custom_listener.py", line 50, in run_ack_function
return self.ack_function(
^^^^^^^^^^^^^^^^^^
File "/Users/victoriamann/Source/slack-workflow-server/platform-bot/src/main.py", line 49, in platform_bot_test
view=modal_view.to_dict()
^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/lib/python3.11/site-packages/slackblocks/views.py", line 54, in to_dict
return self._resolve()
^^^^^^^^^^^^^^^
File "/opt/homebrew/lib/python3.11/site-packages/slackblocks/views.py", line 93, in _resolve
modal_view = super()._resolve()
^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/lib/python3.11/site-packages/slackblocks/views.py", line 44, in _resolve
view["blocks"] = [block._resolve() for block in self.blocks]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/lib/python3.11/site-packages/slackblocks/views.py", line 44, in <listcomp>
view["blocks"] = [block._resolve() for block in self.blocks]
^^^^^^^^^^^^^^^^
File "/opt/homebrew/lib/python3.11/site-packages/slackblocks/blocks.py", line 269, in _resolve
input_block["element"] = self.element._resolve()
^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/lib/python3.11/site-packages/slackblocks/elements.py", line 180, in _resolve
if self.initial_date:
^^^^^^^^^^^^^^^^^
AttributeError: 'DatePicker' object has no attribute 'initial_date'
When the line with the inital_date is uncommentated it works perfectly.
initial_date
is an optional field in the constructor and for the Slack API.
Possible fix: Update the _resolve
method to omit the field when it is set to its default value of None
I have been able to submit section blocks to the Slack API with only fields, and no text attribute. I don't think your class allows that. I propose making the input optional. If you agree, I'd be happy to submit a patch.
slackblocks/slackblocks/blocks.py
Lines 53 to 81 in 8668a01
Hello, i am trying to create this block :
{ "blocks": [ { "type": "divider" }, { "type": "rich_text", "elements": [ { "type": "rich_text_section", "elements": [ { "type": "text", "text": "10:00 - Lorem ipsum blaba\n" } ] }, { "type": "rich_text_list", "style": "bullet", "indent": 0, "border": 0, "elements": [ { "type": "rich_text_section", "elements": [ { "type": "text", "text": "10:00 - Lorem ipsum two " }, { "type": "emoji", "name": "white_check_mark", "unicode": "2705" }, { "type": "text", "text": " " } ] }, { "type": "rich_text_section", "elements": [ { "type": "text", "text": "10:01 - lorem ipsum 3! " }, { "type": "emoji", "name": "white_check_mark", "unicode": "2705" }, { "type": "text", "text": " " } ] } ] } ] }, { "type": "divider" } ] }
I am trying like this:
status = RichTextSection([ RichText(text="10;00 - Lorem ipsum blaba\n", bold=True), RichTextList(style="bullet", elements=( RichTextSection([ RichText("10:00 - Lorem ipsum two \n"), RichText("10:01 - Lorem ipsum3!\n"), ]) )) ])
and i get the following error
} (<class 'slackblocks.rich_text.objects.RichTextList'>)) inconsistent with expected type (<class 'slackblocks.rich_text.elements.RichTextChannel'>, <class 'slackblocks.rich_text.elements.RichTextEmoji'>, <class 'slackblocks.rich_text.elements.RichTextLink'>, <class 'slackblocks.rich_text.elements.RichText'>, <class 'slackblocks.rich_text.elements.RichTextUser'>, <class 'slackblocks.rich_text.elements.RichTextUserGroup'>).
I have no idea how to fix this? Can anyone help me?
Thanks
Hi,
I'm trying to use this module and adding many blocks on the blocks attribute:
message = Message(channel="team-developments",
blocks=[header, divider, intro_section, org_section, divider, doc_section])
but when I try to send the message with the client I receive the following error
TypeError: chat_postMessage() argument after ** must be a mapping, not Message
do you have any hint on how to solve?
thanks
Are checkboxes supported, if not is there any plan to add them?
I can't seem to find any check box classes so im assuming they're not implented
Current implementation mistakenly treats dispatch_action
field of InputBlock
as DispatchActionConfiguration
while API documentation says https://api.slack.com/reference/block-kit/blocks#input_fields it's boolean
According to the docs here, https://api.slack.com/reference/block-kit/composition-objects#text looks like this field has a limitation of 3000 however this implementation has capped this at 75. Is there a reason I might have missed
I wouldn't mind pushing a quick fix for this
Hey,
I'm using the message class and I noticed that __get__
is implemented using resolve in order to get each value,
this means that when **message
is used the message is resolved once for the keys method and then again for every key.
I found it better to just use **message._resolve()
this way a dictionary is returned and _resolve
is called once.
I think making resolve a public function can be a great option to solve this issue.
In the ContextBlock init, the elements arg might have the wrong type.
I think it's currently elements: Optional[List[Union[Element, CompositionObjectType]]] = None,
but I think CompositionObjectType
should be CompositionObject
.
Because CompositionObjectType
refers to the enum type, but the CompositionObject
refers to the actual element/object.
Here's the typeerror being thrown, and Text
should be subclassing CompositionObject
Heyh @nicklambourne!
Love your lib, but I have an issue using StaticMultiSelectMenu with initial_values.
InputBlock(
"Ticket Tags",
element = StaticMultiSelectMenu("ticket_tags",
options=[
Option(Text("Label1", TextType.PLAINTEXT), value="1"),
Option(Text("Label2", TextType.PLAINTEXT), value="2")
],
initial_options=[
Option(Text("Label1", TextType.PLAINTEXT), value="1")
]
)
)
I got the following error,
File "/[REDACTED]/venv/lib/python3.10/site-packages/slackblocks/elements.py", line 520, in __init__
and not isinstance(self.initial_options, List[Option])
File "/usr/lib/python3.10/typing.py", line 994, in __instancecheck__
return self.__subclasscheck__(type(obj))
File "/usr/lib/python3.10/typing.py", line 997, in __subclasscheck__
raise TypeError("Subscripted generics cannot be used with"
TypeError: Subscripted generics cannot be used with class and instance checks
As per my investigation, Python's runtime does not support checking for specific types within generics. In this case a generic (list) as a list of Options. If you use this code you'll see that it gets the same error.
from typing import List
class Option:
pass
# Suppose you have an instance of some options
options = [Option(), Option()]
# And you try to check if 'options' is an instance of a list of Option objects
isinstance(options, List[Option]) # This will raise the error
If we check all objects in the initial_options are actually Option class it works.
if (
options
and self.initial_options
and not all(isinstance(option, Option) for option in self.initial_options) # line 520
):
I modify it on my local machine and it works. What do you think?
slackblocks/slackblocks/elements.py
Line 94 in 6206357
This line is erroring for me and I think the problem is due to len
being called with the Text object rather than the Text object's text
property. There's already a fork of this repo that fixes it.
Feel free to close if there is a better method.
Would it be possible to turn on discussions for the repo so we have a place to chat about upcoming features like: https://slack.com/blog/developers/uploading-private-images-blockkit
SlackClientError("The request to the Slack API failed.\nThe server responded with: {'ok': False, 'error': 'invalid_arguments', 'response_metadata': {'messages': ['[ERROR] failed to match all allowed schemas [json-pointer:/view]', '[ERROR] invalid additional property: submit_disabled [json-pointer:/view]']}}")
Cannot use input elements in slack blocks due to missing Input block in .blocks
ActionsBlock
throws AttributeError: 'ActionsBlock' object has no attribute 'elements'
in Message.json()
.
Minimal reproducer:
from slackblocks import ActionsBlock, SectionBlock, ContextBlock, Button, Text, Message
username = "newuser"
text_section = SectionBlock(f"New signup from {username}")
approve_btn = Button("Approve", action_id="accept_user", value=username, style="primary")
reject_btn = Button("Reject", action_id="reject_user", value=username, style="danger")
actions = ActionsBlock([approve_btn, reject_btn])
msg = Message(channel='signups', blocks=[text_section, actions]).json()
print(msg)
Hello!
We have a few slack apps that rely on app based incoming webhooks. We utilize the WebhookClient class in the SDK to interface with this. It doesn't require a few of the fields, like channel or text. Is there a supported way to pass the blocks in (aside from dropping them into a Message instance and grabbing the blocks from that?)
Hey, I noticed that there are some optional arguments missing for the Message
. I am particularly interested in the following: unfurl_links
and unfurl_media
.
I'll try to add them myself.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.