Giter VIP home page Giter VIP logo

twitch_api's People

Contributors

bors[bot] avatar dinnerbone avatar druppfv avatar emilgardis avatar foxlisk avatar lixlox avatar modprog avatar naumazeredo avatar nerixyz avatar nihaals avatar novelhawk avatar pajlada avatar peddermaster2 avatar peterw-lwl avatar renovate[bot] avatar samoth69 avatar shurizzle avatar silvanshade avatar waridley avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

twitch_api's Issues

What is the expected scope of this project?

By looking at the README, I assumed that this crate was only planning to cover Helix and TMI, but when working on #26, I noticed there was a branch for implementing PubSub, and an issue for supporting graphql. Just so I avoid duplicate work, or know whether to make feature addition PR's here, can you enumerate what all you intend to cover/not cover?

Handle/parse 'Twitch-Eventsub-Message-Type' header

Currently eventsub::Payload only parses the notification message type (all the event types) and has a hardcoded behavior for the VerificationRequest (which has a webhook_callback_verification type in the header) mashed in.

However, there is no such hardcoded thing for the revocation message, which is sent by Twich when for whatever reason the subscription was revoked, and the json they send contains a single subscription: EventSubSubscription field, such object is not parsable by the Payload::parse, although it is valid to expect it to come from Twitch

See https://dev.twitch.tv/docs/eventsub#subscription-revocation

HelixRequestGetError when getting https://api.twitch.tv/helix/subscriptions?

I'm using twitch_api2 0.5.0-alpha. My code:

                let client = reqwest::Client::new();
                let twitch_api_client = TwitchClient::with_client(client);

                let req = GetBroadcasterSubscriptionsRequest::builder()
                .broadcaster_id(broadcaster.id.clone())
                .user_id(vec![msg.sender.id.clone()])
                .build();
                debug!("{:?}", req);
                let req = tokio::spawn(async move { 
                    twitch_api_client.helix.req_get(req, &token).await
                }).await;
                match req {
                    Ok(r) => {
                        println!("{:?}", r)
                    },
                    Err(e) => println!("{:?}", e),
                } 

Output

2021-02-27 23:55:53,173 DEBUG [reqwest::async_impl::client] response '200 OK' for https://api.twitch.tv/helix/subscriptions?broadcaster_id=637140207&user_id=637140207
Err(HelixRequestGetError(DeserializeError("{\"data\":[{\"broadcaster_id\":\"637140207\",\"broadcaster_login\":\"stuck_overflow\",\"broadcaster_name\":\"stuck_overflow\",\"gifter_id\":\"\",\"gifter_login\":\"\",\"gifter_name\":\"\",\"is_gift\":false,\"plan_name\":\"Channel Subscription (stuck_overflow): $24.99 Sub\",\"tier\":\"3000\",\"user_id\":\"637140207\",\"user_name\":\"stuck_overflow\",\"user_login\":\"stuck_overflow\"}]}", Error("unknown field `broadcaster_login`, expected one of `broadcaster_id`, `broadcaster_name`, `is_gift`, `tier`, `plan_name`, `user_id`, `user_name`", line: 1, column: 58), https://api.twitch.tv/helix/subscriptions?broadcaster_id=637140207&user_id=637140207)))

Improve compile time

Compilation with some features makes the crate take ages to compile. Need to figure out why, and try to mitigate it.

Experiments need to be done to figure out what is taking all the time, I suspect codegen is a big timewaste.

Related to #203 and #204

`tag_ids` field in `Stream` after using `get_followed_streams()` can be null

Howdy!

It appears as if the Twitch API will sometimes return null for the tag_ids field in helix::Stream after using get_followed_streams() for whatever reason.

It has occurred twice now within the span of a couple hours, although I unfortunately no longer have access to the first error message.

The second occurred with https://www.twitch.tv/xfsn_saber (I'm unsure if the specific stream actually matters), as upon running it once more ~thirty seconds afterwards, it ran without issue.

Something interesting to note, is that their stream also had "viewer_count": 0, but every other field was normal.

The relevant error message:
Error { path: Path { segments: [Map { key: "data" }, Seq { index: 28 }, Map { key: "tag_ids" }] }, original: Error("invalid type: null, expected a sequence", line: 1, column: 14545) }, https://api.twitch.tv/helix/streams/followed?user_id=19957417, 200))

Using v0.5.0.

Cheers!

make all requests parsable from http responses and make client fully optional

4c5a7b8 added a result function to post requests. This should be added to all specific request traits together with an "assemble" function, making it fully possible to just use the structs and do the sending yourself.

I think this would help @Waridley also with their endeavours ;) separating twitch_api2 and twitch_oauth2

This would mean all the abstraction done in this crate with the Response struct for helix would not be completely arcane and useless for external libs.

This would enable a function fn(R,&str) to retrieve a http::Request<Vec<u8>> for your R: helix::Request to use with your implementation/client, including a token, and a function to parse a http::Response<Vec<u8>> and push out a valid Result<helix::Response<R, D>, SomeError> where the data field on response hold your collection of responses according to D

Figure out a good way to do releases.

Implement a better way to do releases.

I think I'd want a script that does the following

  1. run the script with arguments $bump crates_to_update <release|major|minor|patch>
  2. script changes all references of version number where needed and Cargo.toml of the bumped crates (but only the bumped crate if bump is minor and it's a library we're bumping).
  3. script or user updates changelog if needed.
  4. script creates a commit with the necessary changes, one for each crate.
  5. pr is made, and is then merged. CI on push to master, will for each new commit with commit message with a known structure, ci kicks in and publishes the crate bumped in commit to crates and makes a tag (and a release with changelog , entry, and binary if applicable)
  6. ???
  7. profit!!!

To test this I think it's safest to work in another repo and setup a local crates registry. If act worked on wsl with actions-rs it would be really easy to test, but currently there's an error with cargo action not finding binaries.

I'll chime in here if I make any progress on this.

Gettin `invalid type:null` with SearchCategories on no results

I tried to search for a term that does not exists, this results in the error below:

The Term I search was Mitttttttttttt

Error: HelixRequestGetError(DeserializeError("{\"data\":null,\"pagination\":{}}", Error("invalid type: null, expected a sequence", line: 1, column: 12), https://api.twitch.tv/helix/search/categories?query=Mitttttttttttt&first=20))

RUSTSEC-2021-0060: merged into the `aes` crate

merged into the aes crate

Details
Status unmaintained
Package aes-soft
Version 0.6.4
URL RustCrypto/block-ciphers#200
Date 2021-04-29

The aes-soft crate has been merged into the aes crate. The new repository
location is at:

<https://github.com/RustCrypto/block-ciphers/tree/master/aes>

AES-NI is now autodetected at runtime on i686/x86-64 platforms.
If AES-NI is not present, the aes crate will fallback to a constant-time
portable software implementation.

To force the use of a constant-time portable implementation on these platforms,
even if AES-NI is available, use the new force-soft feature of the aes
crate to disable autodetection.

See advisory page for additional details.

Provide C-bindings

Provide C-bindings for structs and maybe client functions.

Possibly also python

Total field should be a root field instead of being a UsersFollow field

https://github.com/Emilgardis/twitch_api2/blob/0674ab042e601af55cf149990f2b461652c6db00/src/helix/users/get_users_follows.rs#L79-L85

The struct UsersFollow currently contains total which should be in the Response root json object as of the example in twitch docs:

{
   "total": 12345, // <-- The actual field is here
   "data":
   [
      {
         "from_id": "171003792",
         "from_login": "iiisutha067iii",
         "from_name": "IIIsutha067III",
         "to_id": "23161357",
         "to_name": "LIRIK",
         "followed_at": "2017-08-22T22:55:24Z"
         // <-- Right now the library looks for it here
      },
      {
         "from_id": "113627897",
         "from_login": "birdman616",
         "from_name": "Birdman616",
         "to_id": "23161357",
         "to_name": "LIRIK",
         "followed_at": "2017-08-22T22:55:04Z"
         // <-- And also here
      },
      // ...
   ],
   "pagination":{
     "cursor": "eyJiIjpudWxsLCJhIjoiMTUwMzQ0MTc3NjQyNDQyMjAwMCJ9"
   }
}

Get Webhook Subscriptions also does the same thing but is currently ignored in the library. There might be other examples.

Avoid error on empty requests

While using the crate, I noticed some requests will be rejected by twitch because empty. For example:

get_users::GetUsersRequest::builder()
    .id(vec![])
    .login(vec![])
    .build();

will be rejected. But in this case, the response will be empty. Would it be desirable to bypass the entire call and immediately returns a response in this case?
Or, looking at the RequestGet trait, it seems maybe it would be better to detect this error when parsing the response and recover?
What do you think?

features

use all features on docsrs and show how to use them,

also need to make to make reqwest feature able to work with reqwest_client

Factor out client and request abstraction to external crate

Hey there,
I love your really nice and clean work on the client abstraction with possible multiple clients to use and traits for Request, RequestGet etc.

I'm writing a backend that makes requests to multiple different APIs and I'm planning to use your client and request abstractions for this while only implementing the request traits for these APIs.

What came to my mind was, if it wouldn't be nice, to publish the client and request traits in an external crate as api-client, so people would just need to implement the request traits for the corresponding API and could use any client they want to pull the data.

Do you think this is a reasonable thing to do? I think this could be really valuable for the community, as that would make implementing crates for all kinds of APIs much easier. I could also imagine (long-term) porting the outdated kraken Twitch API crate to this foundation (and PR it here), while we could publish the whole thing then under https://crates.io/crates/twitch_api where I would like to add you as an owner as well. Would be lovely to have a wholesome crate of the Twitch API (as you are making your way to it).

RUSTSEC-2021-0059: merged into the `aes` crate

merged into the aes crate

Details
Status unmaintained
Package aesni
Version 0.10.0
URL RustCrypto/block-ciphers#200
Date 2021-04-29

The aesni crate has been merged into the aes crate. The new repository
location is at:

<https://github.com/RustCrypto/block-ciphers/tree/master/aes>

AES-NI is now autodetected at runtime on i686/x86-64 platforms.
If AES-NI is not present, the aes crate will fallback to a constant-time
portable software implementation.

To prevent this fallback (and have absence of AES-NI result in an illegal
instruction crash instead), continue to pass the same RUSTFLAGS which were
previously required for the aesni crate to compile:

RUSTFLAGS=-Ctarget-feature=+aes,+ssse3

See advisory page for additional details.

Add custom req methods to parse responses into custom structs

e.g

#[derive(serde::Deserialize)]
pub struct MyResp {
	pub user_id: twitch_api2::types::UserId,
	pub user_name: String,
}

let resp: MyResp = client.req_get_custom(req, &token).await?.data;
// I'm not sure if this should be a `Response<MyResp>` or if that is even possible.
// I suspect not since Response requires T to be <Request>::Response.
// Could move that requirement to impls only, but not sure how that would affect compile error messages.

This could allow references in responses, and if the user only cares about some fields (or there are new fields not available yet in this library), only get those specific fields.

Implement all helix endpoints

  • Moderation

    • POST https://api.twitch.tv/helix/moderation/enforcements/status Check AutoMod Status
      Determines whether a string message meets the channel’s AutoMod requirements.
    • GET https://api.twitch.tv/helix/moderation/banned Get Banned Users
      Returns all banned and timed-out users in a channel.
    • GET https://api.twitch.tv/helix/moderation/banned/events Get Banned Events
      Returns all user bans and un-bans in a channel.
    • GET https://api.twitch.tv/helix/moderation/moderators Get Moderators
      Returns all moderators in a channel.
    • GET https://api.twitch.tv/helix/moderation/moderators/events Get Moderator Events
      Returns a list of moderators or users added and removed as moderators from a channel.
  • Channels

    • POST https://api.twitch.tv/helix/channels/commercial Start Commercial
      Starts a commercial on a specified channel.
    • GET https://api.twitch.tv/helix/channels Get Channel Information
      Gets channel information for users.
    • PATCH https://api.twitch.tv/helix/channels Modify Channel Information
      Modifies channel information for users.
  • Analytics

    • GET https://api.twitch.tv/helix/analytics/extensions Get Extension Analytics
      Gets a URL that extension developers can use to download analytics reports (CSV files) for their extensions. The URL is valid for 5 minutes. For details about analytics and the fields returned, see the Insights Analytics Guide.
    • GET https://api.twitch.tv/helix/analytics/games Get Game Analytics
      Gets a URL that game developers can use to download analytics reports (CSV files) for their games. The URL is valid for 5 minutes. For detail about analytics and the fields returned, see the Insights Analytics guide.
  • Bits

    • GET https://api.twitch.tv/helix/bits/cheermotes Get Cheermotes
      Retrieves the list of available Cheermotes, animated emotes to which viewers can assign Bits, to cheer in chat. Cheermotes returned are available throughout Twitch, in all Bits-enabled channels.
    • GET https://api.twitch.tv/helix/bits/leaderboard Get Bits Leaderboard
      Gets a ranked list of Bits leaderboard information for an authorized broadcaster.
  • Extensions

    • GET https://api.twitch.tv/helix/extensions/transactions Get Extension Transactions
      Get Extension Transactions allows extension back end servers to fetch a list of transactions that have occurred for their extension across all of Twitch.
  • Clips

    • POST https://api.twitch.tv/helix/clips Create Clip
      Creates a clip programmatically. This returns both an ID and an edit URL for the new clip.
    • GET https://api.twitch.tv/helix/clips Get Clips
      Gets clip information by clip ID (one or more), broadcaster ID (one only), or game ID (one only).
  • Entitlements

    • POST https://api.twitch.tv/helix/entitlements/upload Create Entitlement Grants Upload URL
      Creates a URL where you can upload a manifest file and notify users that they have an entitlement. Entitlements are digital items that users are entitled to use. Twitch entitlements are granted to users gratis or as part of a purchase on Twitch.
    • GET https://api.twitch.tv/helix/entitlements/codes Get Code Status
      Codes are redeemable alphanumeric strings tied only to the bits product. This third-party API allows other parties to redeem codes on behalf of users. Third-party app and extension developers can use the API to provide rewards of bits from within their games.
    • POST https://api.twitch.tv/helix/entitlements/code Redeem Code
      Codes are redeemable alphanumeric strings tied only to the bits product. This third-party API allows other parties to redeem codes on behalf of users. Third-party app and extension developers can use the API to provide rewards of bits from within their games.
  • Games

    • GET https://api.twitch.tv/helix/games/top Get Top Games
      Gets games sorted by number of current viewers on Twitch, most popular first.
    • GET https://api.twitch.tv/helix/games Get Games
      Gets game information by game ID or name.
  • Search

    • GET https://api.twitch.tv/helix/search/categories Search Categories
      Returns a list of games or categories that match the query via name either entirely or partially.
    • GET helix/search/channels Search Channels
      Returns a list of channels (users who have streamed within the past 6 months) that match the query via channel name or description either entirely or partially. Results include both live and offline channels. Online channels will have additional metadata (e.g. started_at, tag_ids). See sample response for distinction.
  • Streams

    • https://api.twitch.tv/helix/streams/key Get Stream Key
      Gets the channel stream key for a user.
    • GET https://api.twitch.tv/helix/streams Get Streams
      Gets information about active streams. Streams are returned sorted by number of current viewers, in descending order. Across multiple pages of results, there may be duplicate or missing streams, as viewers join and leave streams.
    • POST https://api.twitch.tv/helix/streams/markers Create Stream Marker
      Creates a marker in the stream of a user specified by a user ID. A marker is an arbitrary point in a stream that the broadcaster wants to mark; e.g., to easily return to later. The marker is created at the current timestamp in the live broadcast when the request is processed. Markers can be created by the stream owner or editors. The user creating the marker is identified by a Bearer token.
    • GET https://api.twitch.tv/helix/streams/markers Get Stream Markers
      Gets a list of markers for either a specified user’s most recent stream or a specified VOD/video (stream), ordered by recency. A marker is an arbitrary point in a stream that the broadcaster wants to mark; e.g., to easily return to later. The only markers returned are those created by the user identified by the Bearer token.
    • GET https://api.twitch.tv/helix/streams/tags Get Stream Tags
      Gets the list of tags for a specified stream (channel).
      The response has a JSON payload with a data field containing an array of tag elements.
    • PUT https://api.twitch.tv/helix/streams/tags Replace Stream Tags
      Applies specified tags to a specified stream, overwriting any existing tags applied to that stream. If no tags are specified, all tags previously applied to the stream are removed. Automated tags are not affected by this operation.
  • Subscriptions

  • Tags

    • GET https://api.twitch.tv/helix/tags/streams Get All Stream Tags
      Gets the list of all stream tags defined by Twitch, optionally filtered by tag ID(s).
  • Users

    • POST https://api.twitch.tv/helix/users/follows  Create User Follows
      Adds a specified user to the followers of a specified channel.
    • DELETE https://api.twitch.tv/helix/users/follows Delete User Follows
      Deletes a specified user from the followers of a specified channel.
    • GET https://api.twitch.tv/helix/users Get Users
      Gets information about one or more specified Twitch users. Users are identified by optional user IDs and/or login name. If neither a user ID nor a login name is specified, the user is looked up by Bearer token.
    • GET https://api.twitch.tv/helix/users/follows Get Users Follows
      Gets information on follow relationships between two Twitch users. Information returned is sorted in order, most recent follow first. This can return information like “who is qotrok following,” “who is following qotrok,” or “is user X following user Y.”
    • PUT https://api.twitch.tv/helix/users Update User
      Updates the description of a user specified by a Bearer token.
    • GET https://api.twitch.tv/helix/users/extensions/list Get User Extensions
      Gets a list of all extensions (both active and inactive) for a specified user, identified by a Bearer token.
    • GET https://api.twitch.tv/helix/users/extensions Get User Active Extensions
      Gets information about active extensions installed by a specified user, identified by a user ID or Bearer token.
    • PUT https://api.twitch.tv/helix/users/extensions Update User Extensions
      Updates the activation state, extension ID, and/or version number of installed extensions for a specified user, identified by a Bearer token. If you try to activate a given extension under multiple extension types, the last write wins (and there is no guarantee of write order)
  • Videos

    • GET https://api.twitch.tv/helix/videos Get Videos
      Gets video information by video ID (one or more), user ID (one only), or game ID (one only).
  • Webhooks

    • GET https://api.twitch.tv/helix/webhooks/subscriptions Get Webhook Subscriptions
      Gets the Webhook subscriptions of a user identified by a Bearer token, in order of expiration.
  • Hypetrain

    • GET https://api.twitch.tv/helix/hypetrain/events Get Hype Train Events
      Gets the information of the most recent Hype Train of the given channel ID. When there is currently an active Hype Train, it returns information about that Hype Train. When there is currently no active Hype Train, it returns information about the most recent Hype Train.  After 5 days, if no Hype Train has been active, the endpoint will return an empty response.

Provide Streams for Paginated Endpoints

When using paginated endpoints it's not always best to put it in a giant vector first and then return it like it's done currently. Often the items, for example followers, need to be processed in some way on their own (like storing old followers in a file). For this it's better to stream the items into the file (in this case).

So being able to do something like this would be nice:

async fn save_followers(file) -> AnyResult {
  let mut stream = client.stream_followers(...);

  while let Some(follower) = stream.try_next().await? {
    file.save_follower(&follower).await;
  }
  
  Ok(())
}

Builder pattern + methods

Currently, this crate uses the builder pattern to assure a stable API i.e https://github.com/Emilgardis/twitch_api2/blob/e1ba9faff6c72b92f271a888b480a016c334b82a/src/helix/channels.rs#L82-L88

I decided to do it this way due to Twitch not guaranteeing a stable API themselves on documented items, so marking structs as #[non_exhaustive] and using a builder seems better.

However, sometimes this makes things harder to use, as typed-builder does not exactly make structs more accessible.

While this is done for stability, I feel like some structs don't lend themselves to the builder pattern, like the above example.

I want to solve this somehow, without sacrificing API stability.

To do that I think what needs to be done is creating methods for common usages of requests.

Example:

impl GetChannelInformationRequest {
    pub fn get_channel(broadcaster: String) -> GetChannelInformationRequest {
        GetChannelInformationRequest::builder().broadcaster_id(broadcaster).build()
    }
}

or for https://github.com/Emilgardis/twitch_api2/blob/e1ba9faff6c72b92f271a888b480a016c334b82a/src/helix/games.rs#L59-L68

impl GetGamesRequest  {
    pub fn get_game(id: types::CategoryId) -> GetGamesRequest {
        GetGamesRequest::builder().id(vec![id]).build()
    }
}

There is the crate derive_builder that could help also.

Make it possible to use the types in sqlx, diesel, (other?)

The braided types introduced in #133 should derive

#[aliri_braid::braid(serde)]
#[cfg_attr(feature = "sqlx", derive(sqlx::Type), sqlx(transparent))]
#[cfg_attr(feature = "diesel", derive(diesel::FromSqlRow, diesel::AsExpression),]
pub struct UserName;

#[cfg(feature = "diesel")]
impl<DB> diesel::serialize::ToSql<diesel::sql_types::Text, DB> for UserName
where
    DB: diesel::Backend,
    String: diesel::serialize::ToSql<diesel::sql_types::Text, DB>,
{
    fn to_sql<W: std::io::Write>(&self, out: &mut diesel::serialize::Output<W, DB>) -> diesel::serialize::Result {
        (self as &str).to_sql(out)
    }
}

#[cfg(feature = "diesel")]
impl<ST, DB> diesel::deserialize::FromSql<ST, DB> for UserName
where
    DB: Backend,
    String: diesel::deserialize::FromSql<ST, DB>,
{
    fn from_sql(bytes: Option<&DB::RawValue>) -> diesel::deserialize::Result<Self> {
        <String as FromSql<ST, DB>>::from_sql(bytes).map(UserName::new)
    }
}
/// diesel::Queryable, etc...

This will allow users to use the braids better when using sqlx or diesel, exposing the types as strings.

Diesel has support for these with #[diesel(deserialize_as = "sql_types::Text")] on Queryable and #[diesel(serialize_as = "String")] on Insertable, not super nice but should work.

Should look into these crates and/or rust core/std if it would be possible to make this easier on library authors. For example, I can imagine a trait Stringable, which defines a thing which can be represented as a String/&str.

This is unfortunate, because I really like the idea of wrapping the types like this, but the lack of support in the ecosystem is saddening.

Best scenario, would be for sqlx to support a similar macro attr like serde/diesel remote derive or serialize_with and deserialize_with

Tokio v1 support

Attempting to run this library with the version 1 version of Tokio yields the following error:

thread 'main' panicked at 'not currently running on the Tokio runtime.', {..}/tokio-0.2.24/src/runtime/handle.rs:118:28

Considering tokio released its v1 maybe it's time to upgrade the version of tokio as well.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.