Code Monkey home page Code Monkey logo

matrix-rust-sdk's Introduction

Build Status codecov License #matrix-rust-sdk Docs - Main Docs - Stable

matrix-rust-sdk

matrix-rust-sdk is an implementation of a Matrix client-server library in Rust.

Project structure

The rust-sdk consists of multiple crates that can be picked at your convenience:

  • matrix-sdk - High level client library, with batteries included, you're most likely interested in this.
  • matrix-sdk-base - No (network) IO client state machine that can be used to embed a Matrix client in your project or build a full fledged network enabled client lib on top of it.
  • matrix-sdk-crypto - No (network) IO encryption state machine that can be used to add Matrix E2EE support to your client or client library.

Minimum Supported Rust Version (MSRV)

These crates are built with the Rust language version 2021 and require a minimum compiler version of 1.70.

Status

The library is in an alpha state, things that are implemented generally work but the API will change in breaking ways.

If you are interested in using the matrix-sdk now is the time to try it out and provide feedback.

Bindings

Some crates of the matrix-rust-sdk can be embedded inside other environments, like Swift, Kotlin, JavaScript, Node.js etc. Please, explore the bindings/ directory to learn more.

License

Apache-2.0

matrix-rust-sdk's People

Contributors

agraven avatar anderas avatar andrewferr avatar andybalaam avatar billcarsonfr avatar bnjbvr avatar daniel-abramov avatar dependabot[bot] avatar devinr528 avatar dkasak avatar flixcoder avatar florianduros avatar ftilde avatar ganfra avatar gnunicorn avatar hywan avatar ismailgulek avatar jmartinesp avatar johannescpk avatar jplatte avatar jsparber avatar kegsay avatar nimau avatar pixlwave avatar poljar avatar richvdh avatar stefanceriu avatar timokoesters avatar velin92 avatar zecakeh 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  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

matrix-rust-sdk's Issues

Transaction id for room_send

In order to deal with local echo could the user pass a Uuid to AsyncClient::room_send method or create an AsyncClient.transaction_id field and a get method could be used to store the most recent transaction_id created and allow the user to retrieve for filtering local echo out?

Sometimes autojoin fails

When using the code from the autojoin bot, joining sometimes fails with the the error

Oct 08 22:36:31.192 ERROR matrix_sdk_base::models::room: add_member called on event of an already joined user
Autojoining room 
thread 'main' panicked at 'Can't join room: RumaResponse(Http(Known(Error { kind: Forbidden, message: "You are not invited to this room.", status_code: 403 })))', matrix_sdk/examples/autojoin.rs:39:18
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

To Reproduce
setup
Steps to reproduce the behavior:

  1. Setup and start autojoin bot (in this graphic @bot:tchncs.de)
  2. Start a DM room with @bot:tchncs.de from another user (@user:matrix.org) using Element Web
  3. Sometimes @bot:tchncs.de joins the room, sometimes it shows the above error message Can't join room: RumaResponse(Http(Known(Error { kind: Forbidden, message: "You are not invited to this room.", status_code: 403 })))

I used the homeservers mentioned above, but changed the user_id.

Expected behavior
I expect that I can join the room, after receiving the invite. Not sure, if this is the right repo to report this issue.

The `Room` or room state needs to hold `EventIds`

I saw this while poking around the spec and thought it'd be worth making a note of.

However we keep state we need to track the EventId's for certain events an example of this would be rich replies. We need a mapping or way to query the stored "event stream" to get a message from an event id.

Support more API endpoints.

The AsyncClient for now only supports logging in, syncing and sending of messages.

Matrix has much more functionality and those should be added.

An incomplete list is here:

  • Putting room state
  • Joining rooms
  • Leaving rooms
  • Inviting users to a room
  • Kicking users
  • Creating rooms

Some of these are simple enough, and don't need complex arguments (e.g. joining a room), but some of them will need some type of struct that implements a builder pattern if Ruma doesn't already provide this (creating a room).

These can be implemented one at a time.

Subtraction overflow in matrix-sdk-crypto/machine.rs

should_upload_keys subtraction overflowed on me.

match &self.uploaded_signed_key_count {
    Some(count) => {
        let max_keys = self.account.max_one_time_keys().await as u64;
        let key_count = (max_keys / 2) - count.load(Ordering::Relaxed);
            key_count > 0 // should this be an i64 instead of u64?
        }
    None => false,
}

WASM tracking

  • EventEmitter and CryptoStore: Probably requires an alternative trait without Send + Sync (#60)
  • Sleeping panics: Possibly wasm-timer could help fix that. Also Instant is using the instant crate but wasm-timer provides an Instant as well, so could get potentially changed
  • Switch to crates.io version of futures-locks once a new version gets published (asomers/futures-locks#38) (#94)
  • Open PRs to allow olm to compile
  • CryptoStore needs an IndexedDB Implementation to work reliably (prototype: indexeddb-cryptostore) (#414)

Extracted from: #31 (review)

[question] WASM target optional?

Hi!

The addition of WASM pulls quite a bit of dependencies. At a cursory check it seems that is needed for write WASM bots.

What if I don't need WASM? Would it be possible to gate this behind a feature flag?

My understanding may be a bit off, so I'll be happy to receive more context.

And thanks for working on this project!

[Feature] Automatically fetch full members list for rooms when needed

matrix_sdk_base::Room has methods to get the members of a room, but they only return members stored in the StateStore, but the stored list may be incomplete if lazy-loading is used.
The client developer can request the full list from the matrix server if they like, but I think we should do the request for them automatically.

I would suggest adding an boolean argument to Room::joined_user_ids(), Room::active_members(), Room.get_member() , Room::joined_members() for letting the user decide whether to automatically request the full member list, when not already stored, before return any result or return without making a request to the Matrix server.

Moving to depend on the ruma monorepo and ruma-events 0.22

In order to test out the work on the ruma-events macros I've been doing for GSoC I got everything working in matrix-sdk (at least all the tests, I have not gotten to switching rumatui over to the new API). If you want to check it out see what you think here's the PR. I'll make some comments in places that were particularly tricky/odd or just where I wasn't sure about something.

If you have any ideas to make the diff better I wouldn't mind going through again so it's easier to read.

Slow to (re)compile

@martinetd was surprised by the long (re)compilation times: #104 (comment).

I've played around with this locally because I assumed this comes mostly down to the large amount of generated code in Ruma, but surprisingly the ruma-client hello_world example recompiles in roughly 3 seconds on my machine while the matrix_sdk login example takes almost 6 seconds on average.

Here's some things that could help that I haven't tried yet:

  1. installing LLVMs linker, lld and using that by creating .cargo/config.toml with

    # adjust to whatever target you are building for
    [target.x86_64-unknown-linux-gnu]
    rustflags = [
        "-C", "link-arg=-fuse-ld=lld",
    ]
  2. building all dependencies in release mode, which may reduce the size of intermediary artifacts and thus improve relinking time, by adding the following to the Cargo.toml of a project using matrix_sdk (this won't work in the workspace itself because it doesn't apply to workspace crates):

    [profile.dev.package."*"]
    opt-level = 2

    (for more info see the Cargo Book)

Separately from that, I've been thinking about having feature flags in Ruma such that you could choose to only have request-sending + response-receiving generated rather than getting all of request-{sending,receiving} + response-{sending,receiving}.

Switch to using Ruma RoomId and UserId types.

We are using strings for room ids and user ids in many places, this creates friction when making requests since they all expect the higher level ruma-identifier types.

This has been done because we otherwise have friction when we need to compare the room id or user id to a string (e.g. a state key to a user id).

Since https://github.com/ruma/ruma-identifiers/issues/14 will be worked one, we can switch to the higher level types and have easy comparison with strings.

The SDK should use reexported types directly

The SDK in many places uses types defined in other crates which makes it unclear to the user that they are actually reexported by the SDK. I think it would be nice if the SDK would use the reexported types directly so that the docs clearly show which types the developer needs to include and informs the developer about the reexported types.

How to stop syncing when using the EventEmitter API

I would like to run a job that syncs for e.g. 1 minute and then gracefully exits.
I could implement this functionality using sync_with_callback and LoopCtrl, but it's not obvious how to do it using the EventEmitter API.

Is this even possible?

Add a state store.

Matrix clients need to keep the room state in sync with the server, while it is possible to request the state again from the server (using full_state=true for a sync), fetching the state from the server is very taxing to the server.

The base client needs to be able to store its state and we need the ability to restore it once a client user either restores a session or logs in back again. Beside the state the sync token should be stored as well.

The store should be implemented as an async trait so library users can reimplement a store using their preferred database or storage solution. Serializing/deserializing the room structs and just saving the json to a directory as files might be a easy quick solution for a initial store implementation.

Later on we might want to add support to encrypt those files.

Add support to upload and use filters for the relevant endpoints

Filters are nowadays a must to use, most importantly to use member lazy loading since it can be quite slow to handle tens of thousands members for a initial sync.

SyncSettings needs to be expanded to take a filter, ruma-client-api already provides a nice abstraction over filter ids vs filter definitions since both can be used as a filter in the sync.

examples usability

Describe the bug
I understand the examples aren't meant to be useable as is, but it's a starting point for someone taking a look at the project (hi! Please just tell me to GTFO if you can't be bothered or it's too early or whatever, it's much better than no answer -- and sorry it's a bit messy I just spent a couple of hours playing with this all)

  • there is a hardcoded proxy to localhost:8080 in most examples ; and the error isn't exactly obvious:
$ ./emoji_verification https://matrix.somewhere.org test redactedpass
Error: Reqwest(reqwest::Error { kind: Request, url: "https://matrix.somewhere.org/_matrix/client/r0/login", source: hyper::Error(Connect, ConnectError("tcp connect error", Os { code: 111, kind: ConnectionRefused, message: "Connection refused" })) })

It would make sense to write something about the proxy there if possible?
(Note: is Reqwest, with a w, on purpose? I just noticed as I am filling this; that is sneaky...)

  • removing the proxy connects fine, but I get an unknown variant for deserialization ? (line likely a bit off, that's on deserialize())
$ RUST_BACKTRACE=1 ./emoji_verification https://matrix.somewhere.org test redactedpass
thread 'main' panicked at 'Can't deserialize to-device event: Error("unknown variant `m.qr_code.show.v1`, expected `m.sas.v1`", line: 1, column: 42)', matrix_sdk/examples/emoji_verification.rs:75:22

I guess that's some new element feature that would need to be implemented in matrix_sdk_crypto/src/verification/? If I don't try to verify immediately but try to verify later by explicitely picking emoji I can get it to work but that was also a bit surprising

  • I guess that doubles with #16 but while I see that e.g. the autojoin example creates a config directory that will store encryption keys (I think?) I think most people implementing clients/bots would prefer to see a persistent session too so having an example that also stores the access_token/device_id might make sense if possible at all?
    I'm somewhat new to rust, but from what I've looked at it's easy to replace the login() call by restore_login() if you have access token, device id and user id -- but with base_client marked as private I don't see any way of accessing the first two? There's an user_id() wrapper but nothing for the other two.
    I could generate a device-id myself (or write down the one generated by the server by checking on another client) and login with that explicitely but I'd much rather use the access token that can be revoked at any time than my password if possible...

Also, doing all this (always reusing the same config dir + same device id and doing emoji verification once), I can't get the command_bot to see any encrypted message (but it works fine in an unencrypted room) -- I guess it should also listen to EncryptedEventContent? I don't find much about that, would have expected things to be transparently decrypted if keys etc are setup for the matrix-sdk.

  • are there any known real users of the library yet? I might have been a little bit too eager :)

Merge RoomMember and User structs.

Currently we have two structs for room members, User and RoomMember, they both have a events field and RoomMember has a field that holds a User struct.

It would probably make sense to remove the duplicate fields and just merge User into RoomMember.

Rename `Client::add_event_emitter` to `Client::set_event_emitter`

I've had the same issue as another user in #matrix-rust-sdk where I thought that Client::add_event_emitter would keep track of the handlers in some sort of list until I looked at the implementation.

I think that the name set_event_emitter would better convey the fact that there's only one handler at a time.

(Also I think that EventEmitter is a poor name since it handles events, not emits them (other than application specific events, of course))

Storing timelines in the state store

The state store used to be able to store some limited amount of events from the timeline, while the features seemed to be a bit flaky and due to the snapshot based state storage quite limited it was generally useful.

We should reintroduce this feature in the new state store.

Since the new state store keeps stuff out of memory we can without a bad conscience store the whole timeline in it. The state store trait should be expanded to allow this.

For some basic functionality, we'll need:

  1. The ability to store events.
  2. The ability to get an event by its event ID.
  3. The ability to get previous/next events after giving an event id.

How gaps in the timeline should be handled needs to be figured out.

Each sync response will contain a slice of a timeline, the timeline might have been a continuation of the previous sync response, in which case the limited parameter will be set to false. If the timeline doesn't contain all events that happened between two syncs the limited parameter will be set to true and a gap exists in the timeline. More info can be found over here.

We'll likely will need to remember which event belongs to which slice and figure out how to merge slices when we request historic events using the /rooms/$room_id/messages endpoint.

Our room_messages() and similar methods that fetch events from the server should use events from the store if they are available and fill in from the server if not.

The design.md file is outdated

AFAICS, the design.md file is not updated to match the latest changes to the SDK.
It should either get updated or removed. At the very least, it should include some kind of warning.

Cannot login with username, password

First for all, thank you for this useful crate! ๐Ÿ™

I tried to implement a simple echo bot to test-drive the matrix-sdk crate. However I cannot get past the login stage:

const DEFAULT_HOMESERVER_URL: &str = "https://matrix.org";
let homeserver_url = Url::parse(&DEFAULT_HOMESERVER_URL).unwrap();
let username = "myname";
let password = "mypassword";
let client_config = ClientConfig::default();
let client = Client::new_with_config(homeserver_url, client_config)?;
client
    .login(username, password, None, Some("Bot"))
    .await?;

Running this, I get the following error:

Error: can't parse the JSON response as a Matrix response

Is there any way to inspect the JSON response? Any other ideas how to debug this?

Example for reading and writing messages in an encrypted room.

I understand that the matrix-sdk supports end-to-end encryption.

However it's not clear how this could work. A small example showing how to read messages from an encrypted room and how to send messages to an encrypted room would be extremely helpful. ๐Ÿ™

I have looked at the cross_signing_bootstrap example which sounds related, but I am not sure how to proceed.

Depending on matrix-sdk

In order to get the messages feature turned on, I had to

matrix-sdk = "0.1.0"
matrix-sdk-base = "0.1.0" # in order to turn on the default features

in the Cargo.toml. Is this expected?

RoomId to room name

Great crate! It's good to see a rust-skd.

I am messing around with a matrix command line client and have the login functionality down but have hit a bit of an issue with the RoomId to room name or alias mapping.

I found the coresponding js code here but am unsure how exactly to duplicate this, here is my attempt.

I would love to help out if there is anything I could do, I realize it's early days.

A matrix_sdk_base::Room needs a method to get the timeline (RoomEvents)

Currently the user needs to track the RoomEvents themself on every sync, because the sdk doesn't store them (see #138).
It would be nice that a user can retrieve the events directly from the sdk.

I would suggest adding a method to Room that returns the timeline. The method takes an start EventId and an optional end EventId or a limit that specifics how many events the user wants.

    pub async fn get_timeline(&self,
                              start: EventId,
                              end: Option<EventId>,
                              limit: Option<u128>,
                              direction: Direction,
                              ) -> StoreResult<Vec<AnyRoomEvent>> {
        // Get events from store
    } 

I created to track the high-level issue separate form #138, which concerns the store itself.

Have Client logout correctly

Is your feature request related to a problem? Please describe.
Currently if I logout the only thing I can do is drop Client, but that leaves any task running until it ends and it leaves the store in the disk.

Describe the solution you'd like
Add a .logout() async method that handles this in the right way.

Describe alternatives you've considered
Doing that as the user of this library, but then every client would have to implement this functionality again and makes its usage more difficult.

Additional context
We need this in Fractal.

send request sould be able with ruma_api::error::Void

Currently you can only send requests that use error: ruma_client_api::Error (edit: Or ruma_client_api::r0::uiaa::UiaaResponse) in the endpoint declaration.
let request = get_server_version::Request::new();
let response = client.send(request).await.unwrap();
|

To Reproduce
`
use synapse_admin_api::v1::get_server_version;

let client = Client::new(homeserver_url).unwrap();
let request = get_server_version::Request::new();
let response: get_server_version::Response = client.send(request).await.unwrap();
leads to
let response = client.send(request).await.unwrap();
| ^^^^ the trait From<FromHttpResponseError<ruma_api::error::Void>> is not implemented for matrix_sdk::Error
`

Callbacks or trait impl's for allowing user to track events

For clients that want matrix-sdk to handle most of the event and room mutation, only wanting to be notified when something changes certain events a trait could be implemented that tracks all of the events and calls methods from the users struct if it impl's this trait.

/// And in `AsyncClient::sync` there could be a switch case that calls the corresponding method on
/// the `Box<dyn EventEmitter>
pub trait EventEmitter {
    /// Event that alters the name or alias of this room.
    fn on_room_name(&mut self, _: &Room, _: &RoomEvent) {}
    /// Any event that alters the state of the room's members.
    fn on_room_member(&mut self, _: &Room, _: &RoomEvent) {}
}

pub struct MyApp {
    member: Vec<RoomMember>,
    ...
}

impl MyApp {
    fn update_ui(&mut self, members: &HashMap<UserId, RoomMember>) {
        self.members.push(members.get())
        ...
    }
}

impl EventEmitter for MyApp {
    fn on_room_member(&mut self, room: &Room, ev: &RoomEvent) {
        // allowing something like this
        match ev.membership_change() {
            MembershipState::Joined => self.update_ui(&room.members))
        }
    }
    // Methods for all relevant events .
}

async fn main() {
    let client = AsyncClient::with_emitter(Box::new(MyApp));
    loop {
        draw_ui();
        // calling sync will call the proper methods of MyApp
        client.sync();
    }
}

Emoji verification not working

Describe the bug
The emoji verification example provided in the examples doesn't seem to be working. It sends the verification request which I tried accepting from element web. However, after that it is just stuck loading. I have attached a screenshot from element web. And the example script is just stuck with no output.

Please let me know if there's any additional information I can provide to help debug this issue.

To Reproduce
cargo build --examples
./target/debug/examples/emoji_verification https://matrix.org username password

Screenshots
If applicable, add screenshots to help explain your problem.
element

Desktop:

  • OS: Linux samyak 5.10.10-arch1-1 x86_64 GNU/Linux
  • Browser: QtWebEngine 5.15
  • Version: master

Parse DirectEvent and expose info in Room struct

Is your feature request related to a problem? Please describe.
I'd like to handle "direct" rooms differently from channels - it would be trivial to handle these as IRC queries instead of channels and match my usage of IRC better.

Describe the solution you'd like
I get a "m.direct" AnyBasicEvent event on initial sync that ruma deserializes to DirectEvent<DirectEventContent>, defined as follow:

pub struct DirectEventContent(pub BTreeMap<UserId, Vec<RoomId>>);

We could add a "direct" bool (or option bool) to our Room struct, setting true for elements in that map when we see it. Or maybe a direct_target as option<UserId> since we have the info?
I'm not exactly sure what happens to the direct flag when you invite more people in them, let me know if you want me to check... That might lead to weird behaviours.

Describe alternatives you've considered
I can parse the event on my side & store the info on my per roomid hashmap; I'm not sure how useful the feature would be for other consumers of the lib.

Additional context
(If I get an ok-in-principle I'd be happy to try submitting a PR; not sure how long I'll laste but might as well get involved a bit more :P)

Some examples hardcode a proxy.

The proxy setting is mostly used to debug stuff with mitmproxy, while this is useful it shouldn't be the default, we should expose a proxy argument for the CLI.

Downloading and decrypting attachments

The client has the room_send_attachment method to upload, encrypt and send an attachment to a room. However, there doesn't seem to be a (documented) way to download and decrypt an attachment. Is this possible yet?

Some additional examples would be nice.

We currently have a simple example that prints out messages, it would be nice if we had a couple more:

  • Example that automatically responds to some messages, like a bot on !command.
  • Example that automatically joins rooms once invited, this is also for bots mostly, a common feature that needs to be implemented.
  • Example that presents a simple TUI that allows interactive sending of messages.

message encryption stops working after some time

Describe the bug

Ok so there really is something fishy with room keys, things almost always work immediately after logging in from a clean state, but even without restarting I sometimes loose keys one way or another (e.g. in a couple of channels I was typing alternatively from riot and the sdk client, seeing both messages, then now I no longer see messages from the riot device on the sdk client (but other way around works) -- earlier today I could no longer see messages from the sdk client on two different riot sessions (different users), but the sdk client could see both messages.

Once I get in this state I cannot seem to get out of it (e.g. restarting my client with restored session/store doesn't help), I need to log in again with a new device.
When things work restoring the session is fine.

To Reproduce
Steps to reproduce the behavior:

  1. login
  2. use things for a while
  3. magic happens and some message are no longer encrypted properly

Additional context

I wanted to try a room rekey but handle_room_key_request has a big // TODO handle room key requests here -- not sure if that'll work? I can see a RoomKeyRequest(ToDeviceEvent{}) coming in but not sure what to do with it.

There's also the crypto-improvements branch, should I try that first before going further? That looks like it would support it!

I unfortunately didn't have full traces on, so I'll just restart now with wonderdebug enabled and try to dig into that to see if I notice anything... Given how much things changed I'd think it would make sense to switch branch first and try to see if it happens again first, but letting you advise.

Sas::accept causes Element to cancel device verification

Describe the bug
When calling Sas::accept(), Element Desktop and Element Android send an m.key.verification.cancel event as soon as the accept call has returned. Element Android displays a message about a homeserver or a client potentially being compromised. The received cancellation event has the reason "Mismatched commitment".

To Reproduce
Steps to reproduce the behavior:

  1. Listen for a AnyToDeviceEvent::KeyVerificationStart
  2. Call Client::get_verification with the flow id received from the start event
  3. Call Sas::accept on the Sas return by the client

Expected behavior
Other device awaits confirmation and continues verification flow normally

Screenshots
Message displayed by element desktop:

You cancelled verification.

Logs from element desktop:

Error logs from inspector console tab

Desktop (please complete the following information):

  • OS: Ubuntu 20.10
  • Browser: Element desktop
  • Version: 1.7.14

Additional context
Logs from element desktop indicate the cancellation event gets sent when the public keys are received.

Split out the different layers into separate crates.

We have 3 main layers going on here:

  • High level client that knows how to do HTTP requests (AsyncClient)
  • Client state machine that only consumes Ruma responses and updates the internal state
  • Crypto state machine that only consumes Ruma responses that are related to e2ee.

Those 3 are already quite nicely separated but to make the differing components reusable for other projects to consume we should separate them into crates and put them into a workspace.

For this we will need to go through the public API of the different layers and define and document the API better.

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.