mirror of
https://github.com/tinode/chat.git
synced 2025-03-14 10:05:07 +00:00
Merge branch 'devel' into patch-1
This commit is contained in:
51
README.md
51
README.md
@ -4,8 +4,9 @@
|
||||
|
||||
Tinode is *not* XMPP/Jabber. It is *not* compatible with XMPP. It's meant as a replacement for XMPP. On the surface, it's a lot like open source WhatsApp or Telegram.
|
||||
|
||||
Version 0.16. This is beta-quality software: feature-complete but probably with a few bugs. Follow [instructions](INSTALL.md) to install and run. Read [API documentation](docs/API.md).
|
||||
Version 0.16. This is beta-quality software: feature-complete but probably with a few bugs. Follow [instructions](INSTALL.md) to install and run or use one of the cloud services below. Read [API documentation](docs/API.md).
|
||||
|
||||
<img src="docs/app-store.svg" style="opacity:0.35" height=36> <img src="docs/play-store.svg" style="opacity:0.35" height=36> <a href="https://web.tinode.co/"><img src="docs/web-app.svg" height=36></a>
|
||||
|
||||
## Why?
|
||||
|
||||
@ -21,38 +22,44 @@ The goal of this project is to deliver on XMPP's original vision: create a moder
|
||||
* For bugs and feature requests [open an issue](https://github.com/tinode/chat/issues/new).
|
||||
|
||||
|
||||
## Demo
|
||||
## Public service
|
||||
|
||||
A public Tinode service is now available. You can register and use it just like any other instant messenger. Keep in mind that demo accounts present in [sandbox](https://sandbox.tinode.co/) are not available in the public service. You must register an account using valid email in order to use the service.
|
||||
|
||||
### Web
|
||||
|
||||
TinodeWeb, a single page web app, is usually available at https://web.tinode.co/ ([source](https://github.com/tinode/webapp/)).
|
||||
|
||||
Log in as one of `alice`, `bob`, `carol`, `dave`, `frank`. Password is `<login>123`, e.g. login for `alice` is `alice123`. You can discover other users by email or phone by prefixing them with `email:` or `tel:` respectively. Emails are `<login>@example.com`, e.g. `alice@example.com`, phones are `+17025550001` through `+17025550009`.
|
||||
|
||||
If you register a new account you are asked for an email address to send validation code to. For demo purposes, you may use `123456` as a universal validation code. The code you get in the email is also valid.
|
||||
|
||||
[Docker images](https://hub.docker.com/u/tinode/) with the same demo are available.
|
||||
TinodeWeb, a single page web app, is available at https://web.tinode.co/ ([source](https://github.com/tinode/webapp/)). See screenshots below.
|
||||
|
||||
### Android
|
||||
|
||||
[Tindroid](https://github.com/tinode/tindroid) is stable and functional. See the screenshots below. A [debug APK](https://github.com/tinode/tindroid/releases/latest) is provided for convenience.
|
||||
|
||||
### Command Line
|
||||
### iOS
|
||||
|
||||
A text-only [command line client](./tn-cli) implements every possible command.
|
||||
[Tinode for iOS](https://apps.apple.com/app/reference-to-tinodios-here/id123) a.k.a. Tinodios is stable and functional ([source](https://github.com/tinode/ios)). See the screenshots below.
|
||||
|
||||
### Notes
|
||||
### Android
|
||||
|
||||
* The demo server is reset (all data wiped) every night at 3:15am Pacific time. An error message `User not found or offline` means the server was reset while you were connected. If you see it on the web, reload and relogin. On Android log out and re-login. If the database was changed, delete the app then reinstall.
|
||||
[Tinode for Android](https://play.google.com/store/apps/details?id=co.tinode.tindroid) a.k.a. Tindroid is stable and functional ([source](https://github.com/tinode/tindroid)). See the screenshots below.
|
||||
|
||||
* User `Tino` is a [basic chatbot](./chatbot) which responds with a [random quote](http://fortunes.cat-v.org/) to any message.
|
||||
|
||||
## Demo/Sandbox
|
||||
|
||||
A sandboxed demo service is available at https://sandbox.tinode.co/.
|
||||
|
||||
Log in as one of `alice`, `bob`, `carol`, `dave`, `frank`. Password is `<login>123`, e.g. login for `alice` is `alice123`. You can discover other users by email or phone by prefixing them with `email:` or `tel:` respectively. Emails are `<login>@example.com`, e.g. `alice@example.com`, phones are `+17025550001` through `+17025550009`.
|
||||
|
||||
If you register a new account you are asked for an email address to send validation code to. For demo purposes you may use `123456` as a universal validation code. The code you get in the email is also valid.
|
||||
|
||||
### Sandbox Notes
|
||||
|
||||
* The sandbox server is reset (all data wiped) every night at 3:15am Pacific time. An error message `User not found or offline` means the server was reset while you were connected. If you see it on the web, reload and relogin. On Android log out and re-login. If the database was changed, delete the app then reinstall.
|
||||
* Sandbox user `Tino` is a [basic chatbot](./chatbot) which responds with a [random quote](http://fortunes.cat-v.org/) to any message.
|
||||
* As generally accepted, when you register a new account you are asked for an email address. The server will send an email with a verification code to that address and you can use it to validate the account. To make things easier for testing, the server will also accept `123456` as a verification code. Remove line `"debug_response": "123456"` from `tinode.conf` to disable this option.
|
||||
|
||||
* The demo server is configured to use [ACME](https://letsencrypt.org/) TLS [implementation](https://godoc.org/golang.org/x/crypto/acme) with hard-coded requirement for [SNI](https://en.wikipedia.org/wiki/Server_Name_Indication). If you are unable to connect then the most likely reason is your TLS client's missing support for SNI. Use a different client.
|
||||
|
||||
* The demo uses a single minified javascript bundle and minified CSS. The un-minified version is available at https://web.tinode.co/index-dev.html
|
||||
|
||||
* The sandbox server is configured to use [ACME](https://letsencrypt.org/) TLS [implementation](https://godoc.org/golang.org/x/crypto/acme) with hard-coded requirement for [SNI](https://en.wikipedia.org/wiki/Server_Name_Indication). If you are unable to connect then the most likely reason is your TLS client's missing support for SNI. Use a different client.
|
||||
* The default web app loads a single minified javascript bundle and minified CSS. The un-minified version is also available at https://sandbox.tinode.co/index-dev.html
|
||||
* [Docker images](https://hub.docker.com/u/tinode/) with the same demo are available.
|
||||
* You are welcome to test your client software against the sandbox, hack it, etc. No DDoS-ing though please.
|
||||
|
||||
## Features
|
||||
|
||||
@ -136,5 +143,7 @@ Words 'chat' and 'instant messaging' in Chinese, Russian, Persian and a few othe
|
||||
* پیامرسانی فوری گپ
|
||||
* تراسل فوري
|
||||
* Nhắn tin tức thời
|
||||
* Mensageiro instantâneo
|
||||
* Pesan instan
|
||||
* anlık mesajlaşma sohbet
|
||||
* mensageiro instantâneo
|
||||
* pesan instan
|
||||
* mensajería instantánea
|
||||
|
@ -106,8 +106,10 @@ You can specify the following environment variables when issuing `docker run` co
|
||||
| `AWS_REGION` | string | | AWS Region when using `s3` media handler |
|
||||
| `AWS_S3_BUCKET` | string | | Name of the AWS S3 bucket when using `s3` media handler |
|
||||
| `AWS_SECRET_ACCESS_KEY` | string | | AWS [Secret Access Key](https://aws.amazon.com/blogs/security/wheres-my-secret-access-key/) when using `s3` media handler |
|
||||
| `CLUSTER_SELF` | string | | Node name if the server is running in a Tinode cluster |
|
||||
| `DEBUG_EMAIL_VERIFICATION_CODE` | string | | Enable dummy email verification code, e.g. `123456`. Disabled by default (empty string). |
|
||||
| `EXT_CONFIG` | string | | Path to external config file to use instead of the built-in one. If this parameter is used all other variables except `RESET_DB`, `FCM_SENDER_ID`, `FCM_VAPID_KEY` are ignored. |
|
||||
| `EXT_STATIC_DIR` | string | | Path to external directory containing static data (e.g. Tinode Webapp files) |
|
||||
| `FCM_CRED_FILE` | string | | Path to json file with FCM server-side service account credentials which will be used to send push notifications. |
|
||||
| `FCM_SENDER_ID` | string | | FCM sender ID for receiving push notifications in the web client |
|
||||
| `FCM_VAPID_KEY` | string | | Also called 'Web Client certificate' in the FCM console. Required by the web client to receive push notifications. |
|
||||
@ -119,6 +121,7 @@ You can specify the following environment variables when issuing `docker run` co
|
||||
| `SAMPLE_DATA` | string | _see comment_ | File with sample data to load. Default `data.json` when resetting or generating new DB, none when upgrading. Use `-` to disable |
|
||||
| `SMTP_DOMAINS` | string | | White list of email domains; when non-empty, accept registrations with emails from these domains only (email verification). |
|
||||
| `SMTP_HOST_URL` | string | `'http://localhost:6060/'` | URL of the host where the webapp is running (email verification). |
|
||||
| `SMTP_LOGIN` | string | | Optional login to use for authentication with the SMTP server (email verification). If login is missing, `addr-spec` part of `SMTP_SENDER` will be used: e.g. if `SMTP_SENDER` is `'"John Doe" <jdoe@example.com>'`, `jdoe@example.com` will be used as login. |
|
||||
| `SMTP_PASSWORD` | string | | Password to use for authentication with the SMTP server (email verification). |
|
||||
| `SMTP_PORT` | number | | Port number of the SMTP server to use for sending verification emails, e.g. `25` or `587`. |
|
||||
| `SMTP_SENDER` | string | | [RFC 5322](https://tools.ietf.org/html/rfc5322) email address to use in the `FROM` field of verification emails and for authentication with the SMTP server, e.g. `'"John Doe" <jdoe@example.com>'`. |
|
||||
|
@ -2,10 +2,10 @@
|
||||
|
||||
FROM python:slim
|
||||
|
||||
ARG VERSION=0.15
|
||||
ARG VERSION=0.16
|
||||
ENV VERSION=$VERSION
|
||||
|
||||
LABEL maintainer="Gene Sokolov <gene@tinode.co>"
|
||||
LABEL maintainer="Tinode Team <info@tinode.co>"
|
||||
LABEL name="TinodeChatbot"
|
||||
LABEL version=$VERSION
|
||||
|
||||
|
@ -11,10 +11,10 @@
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
ARG VERSION=0.15
|
||||
ARG VERSION=0.16.2
|
||||
ENV VERSION=$VERSION
|
||||
|
||||
LABEL maintainer="Gene Sokolov <gene@tinode.co>"
|
||||
LABEL maintainer="Tinode Team <info@tinode.co>"
|
||||
LABEL name="TinodeChatServer"
|
||||
LABEL version=$VERSION
|
||||
|
||||
|
@ -75,6 +75,7 @@
|
||||
"host_url": "$SMTP_HOST_URL",
|
||||
"smtp_server": "$SMTP_SERVER",
|
||||
"smtp_port": "$SMTP_PORT",
|
||||
"login": "$SMTP_LOGIN",
|
||||
"sender": "$SMTP_SENDER",
|
||||
"sender_password": "$SMTP_PASSWORD",
|
||||
"validation_body_templ": "./templ/email-validation-body.templ",
|
||||
|
@ -41,6 +41,14 @@ else
|
||||
done < config.template
|
||||
fi
|
||||
|
||||
# If external static dir is defined, use it.
|
||||
# Otherwise, fall back to "./static".
|
||||
if [ ! -z "$EXT_STATIC_DIR" ] ; then
|
||||
STATIC_DIR=$EXT_STATIC_DIR
|
||||
else
|
||||
STATIC_DIR="./static"
|
||||
fi
|
||||
|
||||
# Load default sample data when generating or resetting the database.
|
||||
if [[ -z "$SAMPLE_DATA" && "$UPGRADE_DB" = "false" ]] ; then
|
||||
SAMPLE_DATA="$DEFAULT_SAMPLE_DATA"
|
||||
@ -48,11 +56,11 @@ fi
|
||||
|
||||
# If push notifications are enabled, generate client-side firebase config file.
|
||||
if [ ! -z "$FCM_PUSH_ENABLED" ] ; then
|
||||
# Write client config to static/firebase-init.js
|
||||
echo "const FIREBASE_INIT={messagingSenderId: \"$FCM_SENDER_ID\", messagingVapidKey: \"$FCM_VAPID_KEY\"};"$'\n' > static/firebase-init.js
|
||||
# Write client config to $STATIC_DIR/firebase-init.js
|
||||
echo "const FIREBASE_INIT={messagingSenderId: \"$FCM_SENDER_ID\", messagingVapidKey: \"$FCM_VAPID_KEY\"};"$'\n' > $STATIC_DIR/firebase-init.js
|
||||
else
|
||||
# Create an empty firebase-init.js
|
||||
echo "" > static/firebase-init.js
|
||||
echo "" > $STATIC_DIR/firebase-init.js
|
||||
fi
|
||||
|
||||
# Initialize the database if it has not been initialized yet or if data reset/upgrade has been requested.
|
||||
@ -67,5 +75,11 @@ if [ -s /botdata/tino-password ] ; then
|
||||
./credentials.sh /botdata/.tn-cookie < /botdata/tino-password
|
||||
fi
|
||||
|
||||
args=("--config=${CONFIG}" "--static_data=$STATIC_DIR")
|
||||
|
||||
# Maybe set node name in the cluster.
|
||||
if [ ! -z "$CLUSTER_SELF" ] ; then
|
||||
args+=("--cluster_self=$CLUSTER_SELF")
|
||||
fi
|
||||
# Run the tinode server.
|
||||
./tinode --config=${CONFIG} --static_data=static 2> /var/log/tinode.log
|
||||
./tinode "${args[@]}" 2> /var/log/tinode.log
|
||||
|
62
docs/API.md
62
docs/API.md
@ -1,12 +1,12 @@
|
||||
<!-- TOC depthFrom:1 depthTo:6 withLinks:1 updateOnSave:1 orderedList:0 -->
|
||||
|
||||
- [Server API](#server-api)
|
||||
- [How it works?](#how-it-works)
|
||||
- [General considerations](#general-considerations)
|
||||
- [Connecting to the server](#connecting-to-the-server)
|
||||
- [How it Works?](#how-it-works)
|
||||
- [General Considerations](#general-considerations)
|
||||
- [Connecting to the Server](#connecting-to-the-server)
|
||||
- [gRPC](#grpc)
|
||||
- [WebSocket](#websocket)
|
||||
- [Long polling](#long-polling)
|
||||
- [Long Polling](#long-polling)
|
||||
- [Out of Band Large Files](#out-of-band-large-files)
|
||||
- [Users](#users)
|
||||
- [Authentication](#authentication)
|
||||
@ -15,15 +15,16 @@
|
||||
- [Changing Authentication Parameters](#changing-authentication-parameters)
|
||||
- [Resetting a Password, i.e. "Forgot Password"](#resetting-a-password-ie-forgot-password)
|
||||
- [Credential Validation](#credential-validation)
|
||||
- [Access control](#access-control)
|
||||
- [Access Control](#access-control)
|
||||
- [Topics](#topics)
|
||||
- [`me` topic](#me-topic)
|
||||
- [`me` Topic](#me-topic)
|
||||
- [`fnd` and Tags: Finding Users and Topics](#fnd-and-tags-finding-users-and-topics)
|
||||
- [Query language](#query-language)
|
||||
- [Incremental updates to queries](#incremental-updates-to-queries)
|
||||
- [Possible use cases](#possible-use-cases)
|
||||
- [Query Language](#query-language)
|
||||
- [Incremental Updates to Queries](#incremental-updates-to-queries)
|
||||
- [Possible Use Cases](#possible-use-cases)
|
||||
- [Peer to Peer Topics](#peer-to-peer-topics)
|
||||
- [Group Topics](#group-topics)
|
||||
- [`sys` Topic](#sys-topic)
|
||||
- [Using Server-Issued Message IDs](#using-server-issued-message-ids)
|
||||
- [User Agent and Presence Notifications](#user-agent-and-presence-notifications)
|
||||
- [Push Notifications Support](#push-notifications-support)
|
||||
@ -36,7 +37,7 @@
|
||||
- [Downloading](#downloading)
|
||||
- [Push Notifications](#push-notifications)
|
||||
- [Messages](#messages)
|
||||
- [Client to server messages](#client-to-server-messages)
|
||||
- [Client to Server Messages](#client-to-server-messages)
|
||||
- [`{hi}`](#hi)
|
||||
- [`{acc}`](#acc)
|
||||
- [`{login}`](#login)
|
||||
@ -47,7 +48,7 @@
|
||||
- [`{set}`](#set)
|
||||
- [`{del}`](#del)
|
||||
- [`{note}`](#note)
|
||||
- [Server to client messages](#server-to-client-messages)
|
||||
- [Server to Client Messages](#server-to-client-messages)
|
||||
- [`{data}`](#data)
|
||||
- [`{ctrl}`](#ctrl)
|
||||
- [`{meta}`](#meta)
|
||||
@ -58,7 +59,7 @@
|
||||
|
||||
# Server API
|
||||
|
||||
## How it works?
|
||||
## How it Works?
|
||||
|
||||
Tinode is an IM router and a store. Conceptually it loosely follows a publish-subscribe model.
|
||||
|
||||
@ -86,7 +87,7 @@ Changes to topic metadata, such as changes in topic description, or when other u
|
||||
|
||||
When user's `me` topic comes online (i.e. an authenticated session attaches to `me` topic), a `{pres}` packet is sent to `me` topics of all other users, who have peer to peer subscriptions with the first user.
|
||||
|
||||
## General considerations
|
||||
## General Considerations
|
||||
|
||||
Timestamps are always represented as [RFC 3339](http://tools.ietf.org/html/rfc3339)-formatted string with precision up to milliseconds and timezone always set to UTC, e.g. `"2015-10-06T18:07:29.841Z"`.
|
||||
|
||||
@ -96,7 +97,7 @@ The `{data}` packets have server-issued sequential IDs: base-10 numbers starting
|
||||
|
||||
In order to connect requests to responses, client may assign message IDs to all packets set to the server. These IDs are strings defined by the client. Client should make them unique at least per session. The client-assigned IDs are not interpreted by the server, they are returned to the client as is.
|
||||
|
||||
## Connecting to the server
|
||||
## Connecting to the Server
|
||||
|
||||
There are three ways to access the server over the network: websocket, long polling, and [gRPC](https://grpc.io/).
|
||||
|
||||
@ -124,7 +125,7 @@ See definition of the gRPC API in the [proto file](../pbx/model.proto). gRPC API
|
||||
|
||||
Messages are sent in text frames, one message per frame. Binary frames are reserved for future use. By default server allows connections with any value in the `Origin` header.
|
||||
|
||||
### Long polling
|
||||
### Long Polling
|
||||
|
||||
Long polling works over `HTTP POST` (preferred) or `GET`. In response to client's very first request server sends a `{ctrl}` message containing `sid` (session ID) in `params`. Long polling client must include `sid` in every subsequent request either in the URL or in the request body.
|
||||
|
||||
@ -233,7 +234,7 @@ If certain credentials are required, then user must maintain them in validated s
|
||||
Credentials are initially assigned at registration time by sending an `{acc}` message, added using `{set topic="me"}`, deleted using `{del topic="me"}`, and queries by `{get topic="me"}` messages. Credentials are verified by the client by sending either a `{login}` or an `{acc}` message.
|
||||
|
||||
|
||||
### Access control
|
||||
### Access Control
|
||||
|
||||
Access control manages user's access to topics through access control lists (ACLs) or bearer tokens (_bearer tokens are not implemented as of version 0.15_).
|
||||
|
||||
@ -278,9 +279,9 @@ User-dependent topic properties:
|
||||
|
||||
Topic usually have subscribers. One the the subscribers may be designated as topic owner (`O` access permission) with full access permissions. The list of subscribers can be queries with a `{get what="sub"}` message. The list of subscribers is returned in a `sub` section of a `{meta}` message.
|
||||
|
||||
### `me` topic
|
||||
### `me` Topic
|
||||
|
||||
Topic `me` is automatically created for every user at the account creation time. It serves as means for account updates, receiving presence notification from people and topics of interest, invites to join topics, requests to approve subscription for topics where this user is a manager (has `S` permission). Topic `me` has no owner. The topic cannot be deleted or unsubscribed from. One can leave the topic which will stop all relevant communication and indicate that the user is offline (although the user may still be logged in and may continue to use other topics).
|
||||
Topic `me` is automatically created for every user at the account creation time. It serves as means of managing account information, receiving presence notification from people and topics of interest. Topic `me` has no owner. The topic cannot be deleted or unsubscribed from. One can `leave` the topic which will stop all relevant communication and indicate that the user is offline (although the user may still be logged in and may continue to use other topics).
|
||||
|
||||
Joining or leaving `me` generates a `{pres}` presence update sent to all users who have peer to peer topics with the given user and `P` permissions set.
|
||||
|
||||
@ -318,7 +319,7 @@ Topic `fnd` is read-only. `{pub}` messages to `fnd` are rejected.
|
||||
|
||||
[Plugins](../pbx) support `Find` service which can be used to replace default search with a custom one.
|
||||
|
||||
#### Query language
|
||||
#### Query Language
|
||||
|
||||
Tinode query language is used to define search queries for finding users and topics. The query is a string containing tags separated by spaces or commas. Tags are strings - individual query terms which are matched against user's or topic's tags. The tags can be written in an RTL language but the query as a whole is parsed left to right. Spaces are treated as the `AND` operator, commas (as well as commas preceded and/or followed by a space) as the `OR` operator. The order of operators is ignored: all `AND` tags are grouped together, all `OR` tags are grouped together. `OR` takes precedence over `AND`: if a tag is preceded of followed by a comma, it's an `OR` tag, otherwise an `AND`. For example, `a AND b OR c` is rewritten as `(b OR c) AND a`.
|
||||
|
||||
@ -331,13 +332,13 @@ Tags containing spaces or commas must be enclosed in double quotes (`"`, `\u0022
|
||||
* `flowers travel, puppies`: find topics or users which contain `flowers` and either `travel` or `puppies`, i.e. `(travel OR puppies) AND flowers`.
|
||||
* `flowers, travel puppies, kittens`: find topics or users which contain either one of `flowers`, `travel`, `puppies`, or `kittens`, i.e. `flowers OR travel OR puppies OR kittens`. The space between `travel` and `puppies` is treated as `OR` due to `OR` taking precedence over `AND`.
|
||||
|
||||
#### Incremental updates to queries
|
||||
#### Incremental Updates to Queries
|
||||
|
||||
Queries, particularly `fnd.private` could be arbitrarily large, limited only by the message size and by the underlying database. Instead of rewriting the entire query to add or remove a tag, tag can be added or removed incrementally.
|
||||
|
||||
The incremental update request is processed left to right. It may contain the same tag multiple times, i.e. `-tag+tag` is a valid request.
|
||||
|
||||
#### Possible use cases
|
||||
#### Possible Use Cases
|
||||
* Restricting users to organisations.
|
||||
An immutable tag(s) may be assigned to the user which denotes the organisation the user belongs to. When the user searches for other users or topics, the search can be restricted to always contain the tag. This approach can be used to segment users into organisations with limited visibility into each other.
|
||||
|
||||
@ -366,6 +367,9 @@ A group topic is created by sending a `{sub}` message with the topic field set t
|
||||
|
||||
A user joining or leaving the topic generates a `{pres}` message to all other users who are currently in the joined state with the topic.
|
||||
|
||||
### `sys` Topic
|
||||
|
||||
The `sys` topic serves as an always available channel of communication with the system administrators. A normal non-root user cannot subscribe to `sys` but can publish to it without subscription. Existing clients use this channel to report abuse by sending a Drafty-formatted `{pub}` message with the report as JSON attachment. A root user can subscribe to `sys` topic. Once subscribed, the root user will receive messages sent to `sys` topic by other users.
|
||||
|
||||
## Using Server-Issued Message IDs
|
||||
|
||||
@ -556,13 +560,13 @@ data needs to be cleared, use a string with a single Unicode DEL character "
|
||||
|
||||
Any unrecognized fields are silently ignored by the server.
|
||||
|
||||
### Client to server messages
|
||||
### Client to Server Messages
|
||||
|
||||
#### `{hi}`
|
||||
|
||||
Handshake message client uses to inform the server of its version and user agent. This message must be the first that
|
||||
the client sends to the server. Server responds with a `{ctrl}` which contains server build `build`, wire protocol version `ver`, and
|
||||
session ID `sid` in case of long polling, all in `ctrl.params`.
|
||||
the client sends to the server. Server responds with a `{ctrl}` which contains server build `build`, wire protocol version `ver`,
|
||||
session ID `sid` in case of long polling, as well as server constraints, all in `ctrl.params`.
|
||||
|
||||
```js
|
||||
hi: {
|
||||
@ -834,9 +838,9 @@ get: {
|
||||
ims: "2015-10-06T18:07:30.038Z", // timestamp, "if modified since" - return
|
||||
// public and private values only if at least one of them has been
|
||||
// updated after the stated timestamp, optional
|
||||
user: "usr2il9suCbuko", // string, return results for a single user,
|
||||
user: "usr2il9suCbuko", // string, return results for a single user,
|
||||
// any topic other than 'me', optional
|
||||
topic: "usr2il9suCbuko", // string, return results for a single topic,
|
||||
topic: "usr2il9suCbuko", // string, return results for a single topic,
|
||||
// 'me' topic only, optional
|
||||
limit: 20 // integer, limit the number of returned objects
|
||||
},
|
||||
@ -844,9 +848,9 @@ get: {
|
||||
// Optional parameters for {get what="data"}
|
||||
data: {
|
||||
since: 123, // integer, load messages with server-issued IDs greater or equal
|
||||
// to this (inclusive/closed), optional
|
||||
// to this (inclusive/closed), optional
|
||||
before: 321, // integer, load messages with server-issed sequential IDs less
|
||||
// than this (exclusive/open), optional
|
||||
// than this (exclusive/open), optional
|
||||
limit: 20, // integer, limit the number of returned objects, default: 32,
|
||||
// optional
|
||||
},
|
||||
@ -1010,7 +1014,7 @@ The `read` and `recv` notifications may optionally include `unread` value which
|
||||
</p>
|
||||
|
||||
|
||||
### Server to client messages
|
||||
### Server to Client Messages
|
||||
|
||||
Messages to a session generated in response to a specific request contain an `id` field equal to the id of the
|
||||
originating message. The `id` is not interpreted by the server.
|
||||
|
102
docs/app-store.svg
Normal file
102
docs/app-store.svg
Normal file
@ -0,0 +1,102 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 169 56" enable-background="new 0 0 169 56" xml:space="preserve">
|
||||
<g id="Page-1">
|
||||
<g id="OG_UI_FIRECHAT" transform="translate(-438.000000, -406.000000)">
|
||||
<g id="Download_on_the_App_Store_Badge_US-UK_RGB_blk_092917" transform="translate(438.000000, 406.000000)">
|
||||
<g id="Group_1_">
|
||||
<g id="Shape">
|
||||
<path fill="#A6A6A6" d="M155.1,0H13.4c-0.5,0-1,0-1.5,0c-0.4,0-0.9,0-1.3,0C9.6,0,8.7,0.1,7.8,0.3C6.8,0.4,5.9,0.7,5.1,1.1
|
||||
c-0.8,0.4-1.6,1-2.3,1.7C2.1,3.5,1.6,4.2,1.2,5.1C0.7,5.9,0.4,6.8,0.3,7.7C0.1,8.7,0,9.6,0,10.5c0,0.4,0,0.9,0,1.3v32.4
|
||||
c0,0.4,0,0.9,0,1.3c0,0.9,0.1,1.9,0.3,2.8c0.2,0.9,0.5,1.8,0.9,2.7c0.4,0.8,1,1.6,1.7,2.3c0.7,0.7,1.4,1.2,2.3,1.7
|
||||
c0.8,0.4,1.7,0.7,2.7,0.9c0.9,0.2,1.9,0.2,2.8,0.2c0.4,0,0.9,0,1.3,0c0.5,0,1,0,1.5,0h141.7c0.5,0,1,0,1.5,0c0.4,0,0.9,0,1.3,0
|
||||
c0.9,0,1.9-0.1,2.8-0.2c0.9-0.2,1.8-0.5,2.7-0.9c0.8-0.4,1.6-1,2.3-1.7c0.7-0.7,1.2-1.4,1.7-2.3c0.4-0.8,0.7-1.7,0.9-2.7
|
||||
c0.2-0.9,0.2-1.9,0.3-2.8c0-0.4,0-0.9,0-1.3c0-0.5,0-1,0-1.5V13.4c0-0.5,0-1,0-1.5c0-0.4,0-0.9,0-1.3c0-0.9-0.1-1.9-0.3-2.8
|
||||
c-0.2-0.9-0.4-1.8-0.9-2.7c-0.9-1.7-2.2-3.1-3.9-3.9c-0.8-0.4-1.8-0.7-2.7-0.9c-0.9-0.2-1.9-0.2-2.8-0.2c-0.4,0-0.9,0-1.3,0
|
||||
C156.1,0,155.6,0,155.1,0L155.1,0z"/>
|
||||
<path d="M11.9,54.8c-0.4,0-0.8,0-1.3,0c-0.9,0-1.8-0.1-2.6-0.2c-0.8-0.1-1.6-0.4-2.3-0.8c-0.7-0.4-1.4-0.8-2-1.4
|
||||
c-0.6-0.6-1.1-1.2-1.4-2c-0.4-0.7-0.6-1.5-0.8-2.3c-0.1-0.9-0.2-1.7-0.2-2.6c0-0.3,0-1.3,0-1.3V11.8c0,0,0-1,0-1.3
|
||||
c0-0.9,0.1-1.8,0.2-2.6c0.1-0.8,0.4-1.6,0.8-2.3c0.4-0.7,0.9-1.4,1.4-2c0.6-0.6,1.2-1.1,2-1.4C6.4,1.9,7.2,1.6,8,1.5
|
||||
c0.9-0.1,1.8-0.2,2.6-0.2l1.3,0h144.7l1.3,0c0.9,0,1.8,0.1,2.6,0.2c0.8,0.1,1.6,0.4,2.4,0.8c1.5,0.7,2.7,1.9,3.4,3.4
|
||||
c0.4,0.7,0.6,1.5,0.8,2.3c0.1,0.9,0.2,1.8,0.2,2.6c0,0.4,0,0.8,0,1.2c0,0.5,0,1,0,1.5v29.3c0,0.5,0,1,0,1.5c0,0.5,0,0.9,0,1.3
|
||||
c0,0.9-0.1,1.7-0.2,2.6c-0.1,0.8-0.4,1.6-0.8,2.3c-0.4,0.7-0.9,1.4-1.4,1.9c-0.6,0.6-1.2,1.1-2,1.4c-0.7,0.4-1.5,0.6-2.3,0.8
|
||||
c-0.9,0.1-1.8,0.2-2.6,0.2c-0.4,0-0.8,0-1.3,0l-1.5,0L11.9,54.8z"/>
|
||||
</g>
|
||||
<g id="_Group_" transform="translate(13.675000, 12.200000)">
|
||||
<g id="_Group_2">
|
||||
<g id="_Group_3">
|
||||
<path id="_Path_" fill="#FFFFFF" d="M21.2,16.2c0-2.4,1.3-4.6,3.3-5.8c-1.3-1.8-3.4-3-5.6-3c-2.4-0.2-4.7,1.4-5.9,1.4
|
||||
C11.8,8.8,10,7.4,8,7.5c-2.6,0.1-5,1.5-6.3,3.8C-1.1,16,1,22.8,3.6,26.6c1.3,1.9,2.8,3.9,4.8,3.9c2-0.1,2.7-1.2,5-1.2
|
||||
c2.3,0,3,1.2,5.1,1.2c2.1,0,3.4-1.9,4.7-3.7c0.9-1.3,1.7-2.8,2.1-4.3C22.8,21.3,21.2,18.9,21.2,16.2z"/>
|
||||
<path id="_Path_2" fill="#FFFFFF" d="M17.4,4.9c1.1-1.4,1.7-3.1,1.6-4.9c-1.7,0.2-3.4,1-4.5,2.3c-1.1,1.3-1.7,3-1.6,4.7
|
||||
C14.6,7.1,16.3,6.3,17.4,4.9z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Group" transform="translate(34.208333, 12.600000)">
|
||||
<path id="Shape_1_" fill="#FFFFFF" d="M11.7,13.2H5l-1.6,4.7H0.6L6.9,0.5h2.9l6.3,17.4h-2.9L11.7,13.2z M5.7,11H11L8.4,3.4
|
||||
H8.3L5.7,11z"/>
|
||||
<path id="Shape_2_" fill="#FFFFFF" d="M29.8,11.6c0,3.9-2.1,6.5-5.3,6.5c-1.7,0.1-3.2-0.8-4-2.2h-0.1v6.3h-2.6V5.2h2.5v2.1h0
|
||||
c0.8-1.4,2.4-2.3,4.1-2.2C27.7,5.1,29.8,7.6,29.8,11.6z M27.1,11.6c0-2.6-1.3-4.3-3.4-4.3c-2,0-3.3,1.7-3.3,4.3
|
||||
c0,2.6,1.3,4.3,3.3,4.3C25.8,15.8,27.1,14.1,27.1,11.6z"/>
|
||||
<path id="Shape_3_" fill="#FFFFFF" d="M43.8,11.6c0,3.9-2.1,6.5-5.3,6.5c-1.7,0.1-3.2-0.8-4-2.2h-0.1v6.3h-2.6V5.2h2.5v2.1h0
|
||||
c0.8-1.4,2.4-2.3,4.1-2.2C41.7,5.1,43.8,7.6,43.8,11.6z M41.1,11.6c0-2.6-1.3-4.3-3.4-4.3c-2,0-3.3,1.7-3.3,4.3
|
||||
c0,2.6,1.3,4.3,3.3,4.3C39.8,15.8,41.1,14.1,41.1,11.6L41.1,11.6z"/>
|
||||
<path id="Shape_4_" fill="#FFFFFF" d="M53.1,13.1c0.2,1.7,1.9,2.9,4.2,2.9c2.2,0,3.8-1.1,3.8-2.7c0-1.3-1-2.2-3.2-2.7L55.6,10
|
||||
c-3.2-0.8-4.7-2.3-4.7-4.7c0-3,2.6-5.1,6.4-5.1c3.7,0,6.2,2.1,6.3,5.1h-2.6c-0.2-1.7-1.6-2.8-3.7-2.8c-2.1,0-3.6,1.1-3.6,2.6
|
||||
c0,1.2,0.9,2,3.2,2.5l1.9,0.5c3.6,0.8,5.1,2.3,5.1,4.8c0,3.3-2.6,5.3-6.8,5.3c-3.9,0-6.5-2-6.7-5.1L53.1,13.1z"/>
|
||||
<path id="Shape_5_" fill="#FFFFFF" d="M69.5,2.2v3h2.4v2.1h-2.4v7c0,1.1,0.5,1.6,1.6,1.6c0.3,0,0.6,0,0.9-0.1v2
|
||||
c-0.5,0.1-1,0.1-1.5,0.1c-2.6,0-3.6-1-3.6-3.4V7.3H65V5.2h1.9v-3H69.5z"/>
|
||||
<path id="Shape_6_" fill="#FFFFFF" d="M73.3,11.6c0-4,2.4-6.5,6-6.5c3.7,0,6,2.5,6,6.5c0,4-2.3,6.5-6,6.5
|
||||
C75.7,18.1,73.3,15.6,73.3,11.6z M82.8,11.6c0-2.7-1.3-4.4-3.4-4.4c-2.1,0-3.4,1.6-3.4,4.4c0,2.7,1.3,4.3,3.4,4.3
|
||||
C81.5,15.9,82.8,14.3,82.8,11.6L82.8,11.6z"/>
|
||||
<path id="Shape_7_" fill="#FFFFFF" d="M87.6,5.2h2.5v2.2h0.1C90.5,6,91.8,5,93.2,5.1c0.3,0,0.6,0,0.9,0.1v2.4
|
||||
c-0.4-0.1-0.8-0.2-1.2-0.2c-0.8,0-1.5,0.3-2,0.8c-0.5,0.6-0.8,1.3-0.7,2.1v7.5h-2.6L87.6,5.2z"/>
|
||||
<path id="Shape_8_" fill="#FFFFFF" d="M106.2,14.2c-0.4,2.3-2.6,3.9-5.5,3.9c-3.7,0-6-2.5-6-6.4c0-4,2.3-6.6,5.9-6.6
|
||||
c3.5,0,5.7,2.4,5.7,6.3v0.9h-9v0.2c-0.1,0.9,0.2,1.9,0.9,2.6s1.6,1.1,2.5,1c1.3,0.1,2.5-0.6,2.9-1.8L106.2,14.2z M97.3,10.4
|
||||
l6.4,0c0-0.8-0.3-1.7-0.9-2.3c-0.6-0.6-1.4-0.9-2.3-0.9c-0.9,0-1.7,0.3-2.3,0.9C97.7,8.7,97.3,9.5,97.3,10.4z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="_Group_4" transform="translate(49.000000, 11.000000)">
|
||||
<g id="Group_2_">
|
||||
<path id="Shape_9_" fill="#FFFFFF" d="M4,1C5.1,1,6.2,1.4,7,2.2s1.1,1.9,1,3.1C8,8,6.5,9.6,4,9.6h-3V1H4z M2.3,8.4h1.6
|
||||
c0.8,0,1.6-0.3,2.1-0.9s0.8-1.4,0.7-2.2C6.7,4.5,6.5,3.7,6,3.1S4.6,2.2,3.9,2.2H2.3V8.4z"/>
|
||||
<path id="Shape_10_" fill="#FFFFFF" d="M9.4,6.3c-0.1-1.2,0.4-2.3,1.4-2.9s2.2-0.6,3.2,0s1.5,1.7,1.4,2.9
|
||||
C15.6,7.5,15,8.6,14,9.2c-1,0.6-2.2,0.6-3.2,0C9.9,8.6,9.3,7.5,9.4,6.3z M14.2,6.3c0-1.4-0.6-2.2-1.7-2.2
|
||||
c-1.1,0-1.7,0.8-1.7,2.2c0,1.4,0.6,2.2,1.7,2.2C13.5,8.6,14.2,7.7,14.2,6.3L14.2,6.3z"/>
|
||||
<polygon id="Shape_11_" fill="#FFFFFF" points="23.4,9.6 22.1,9.6 20.8,4.8 20.7,4.8 19.4,9.6 18.1,9.6 16.4,3.1 17.6,3.1
|
||||
18.8,8 18.9,8 20.2,3.1 21.4,3.1 22.7,8 22.8,8 23.9,3.1 25.2,3.1 "/>
|
||||
<path id="Shape_12_" fill="#FFFFFF" d="M26.7,3.1h1.2v1H28c0.3-0.8,1.1-1.2,1.9-1.1c0.6,0,1.2,0.2,1.7,0.7
|
||||
c0.4,0.5,0.6,1.1,0.5,1.7v4.2h-1.3V5.7c0-1-0.4-1.5-1.4-1.5c-0.4,0-0.8,0.1-1.1,0.5C28,5,27.9,5.4,27.9,5.8v3.8h-1.3L26.7,3.1z
|
||||
"/>
|
||||
<polygon id="Shape_13_" fill="#FFFFFF" points="34.1,0.6 35.3,0.6 35.3,9.6 34.1,9.6 "/>
|
||||
<path id="Shape_14_" fill="#FFFFFF" d="M37.1,6.3c-0.1-1.2,0.4-2.3,1.4-2.9s2.2-0.6,3.2,0s1.5,1.7,1.4,2.9
|
||||
c0.1,1.2-0.4,2.3-1.4,2.9c-1,0.6-2.2,0.6-3.2,0C37.5,8.6,37,7.5,37.1,6.3z M41.8,6.3c0-1.4-0.6-2.2-1.7-2.2
|
||||
c-1.1,0-1.7,0.8-1.7,2.2c0,1.4,0.6,2.2,1.7,2.2C41.2,8.6,41.8,7.7,41.8,6.3L41.8,6.3z"/>
|
||||
<path id="Shape_15_" fill="#FFFFFF" d="M44.4,7.7c0-1.2,0.9-1.8,2.4-1.9l1.7-0.1V5.2c0-0.7-0.4-1.1-1.3-1.1
|
||||
c-0.7,0-1.2,0.3-1.3,0.7h-1.2C44.8,3.7,45.8,3,47.3,3c1.6,0,2.5,0.8,2.5,2.2v4.4h-1.2V8.7h-0.1c-0.4,0.7-1.1,1-1.9,1
|
||||
C46,9.7,45.5,9.6,45,9.2C44.6,8.8,44.4,8.3,44.4,7.7z M48.5,7.2V6.7l-1.6,0.1c-0.9,0.1-1.3,0.4-1.3,0.9c0,0.6,0.5,0.9,1.2,0.9
|
||||
c0.4,0,0.8-0.1,1.1-0.4C48.3,8,48.5,7.6,48.5,7.2z"/>
|
||||
<path id="Shape_16_" fill="#FFFFFF" d="M51.4,6.3c0-2,1-3.3,2.6-3.3c0.8,0,1.6,0.4,2,1.1h0.1V0.6h1.3v8.9h-1.2v-1h-0.1
|
||||
c-0.4,0.7-1.2,1.2-2,1.1C52.4,9.7,51.4,8.4,51.4,6.3z M52.7,6.3c0,1.4,0.6,2.2,1.7,2.2c1.1,0,1.7-0.8,1.7-2.2
|
||||
c0-1.3-0.7-2.2-1.7-2.2C53.3,4.2,52.7,5,52.7,6.3L52.7,6.3z"/>
|
||||
<path id="Shape_17_" fill="#FFFFFF" d="M62.6,6.3C62.4,5.2,63,4.1,64,3.5c1-0.6,2.2-0.6,3.2,0c1,0.6,1.5,1.7,1.4,2.9
|
||||
c0.1,1.2-0.4,2.3-1.4,2.9c-1,0.6-2.2,0.6-3.2,0C63,8.6,62.4,7.5,62.6,6.3z M67.3,6.3c0-1.4-0.6-2.2-1.7-2.2
|
||||
c-1.1,0-1.7,0.8-1.7,2.2c0,1.4,0.6,2.2,1.7,2.2C66.6,8.6,67.3,7.7,67.3,6.3z"/>
|
||||
<path id="Shape_18_" fill="#FFFFFF" d="M70.2,3.1h1.2v1h0.1c0.3-0.8,1.1-1.2,1.9-1.1c0.6,0,1.2,0.2,1.7,0.7s0.6,1.1,0.5,1.7
|
||||
v4.2h-1.3V5.7c0-1-0.4-1.5-1.4-1.5c-0.4,0-0.8,0.1-1.1,0.5c-0.3,0.3-0.4,0.7-0.4,1.2v3.8h-1.3V3.1z"/>
|
||||
<path id="Shape_19_" fill="#FFFFFF" d="M82.8,1.5v1.6h1.4v1.1h-1.4v3.3c0,0.7,0.3,1,0.9,1c0.2,0,0.3,0,0.5,0v1.1
|
||||
c-0.2,0-0.5,0.1-0.7,0.1c-1.4,0-2-0.5-2-1.7V4.2h-1V3.2h1V1.5H82.8z"/>
|
||||
<path id="Shape_20_" fill="#FFFFFF" d="M85.9,0.6h1.2v3.5h0.1c0.3-0.8,1.1-1.2,1.9-1.2c0.6,0,1.2,0.2,1.7,0.7s0.6,1.1,0.5,1.7
|
||||
v4.2h-1.3V5.7c0-1-0.5-1.5-1.4-1.5c-0.4,0-0.9,0.1-1.2,0.4s-0.5,0.8-0.4,1.2v3.8h-1.3L85.9,0.6z"/>
|
||||
<path id="Shape_21_" fill="#FFFFFF" d="M98.7,7.8c-0.4,1.2-1.5,2-2.8,1.9c-0.8,0-1.7-0.3-2.2-1c-0.6-0.6-0.8-1.5-0.7-2.3
|
||||
c-0.1-0.9,0.1-1.7,0.7-2.4s1.4-1,2.2-1c1.8,0,2.8,1.2,2.8,3.2v0.4h-4.5v0.1c0,0.5,0.1,0.9,0.4,1.3c0.3,0.4,0.8,0.5,1.3,0.5
|
||||
c0.6,0.1,1.2-0.2,1.5-0.8L98.7,7.8z M94.2,5.8h3.2c0-0.4-0.1-0.9-0.4-1.2c-0.3-0.3-0.7-0.5-1.1-0.5c-0.4,0-0.9,0.2-1.2,0.5
|
||||
C94.4,4.9,94.2,5.3,94.2,5.8L94.2,5.8z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 8.7 KiB |
@ -33,9 +33,9 @@ If you are using the [Docker image](https://hub.docker.com/u/tinode):
|
||||
4. Add `google-services.json` to [Tindroid](/tinode/tindroid/#push_notifications), `GoogleService-Info.plist` to [Tinodios](/tinode/ios/#push_notifications), recompile the apps.
|
||||
|
||||
|
||||
### Q: How can new users be added to Tinode?
|
||||
### Q: How can new users be added to Tinode?<br/>
|
||||
**A**: There are three ways to create accounts:
|
||||
* A user can create a new account using client-side UI.
|
||||
* A user can create a new account using one of the applications (web, Android, iOS).
|
||||
* A new account can be created using [tn-cli](../tn-cli/) (`acc` command). The process can be scripted.
|
||||
* If the user already exists in an external database, the Tinode account can be automatically created on the first login using the [rest authenticator](../server/auth/rest/).
|
||||
|
||||
|
178
docs/play-store.svg
Normal file
178
docs/play-store.svg
Normal file
@ -0,0 +1,178 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 2000 659" enable-background="new 0 0 2000 659" xml:space="preserve">
|
||||
<line fill="#FFFFFF" x1="1645.1" y1="5370.3" x2="205.1" y2="5370.3"/>
|
||||
<path fill="#A2A2A1" d="M1001.2,659c-306.9,0-613.8,0-920.6,0.1c-17.3,0-32.9-4.3-46.8-15.8c-19.9-16.6-30.6-38.9-30.6-66.8
|
||||
C3,411.9,3,247.3,3.1,82.7c0-39,25.3-73.1,59.6-80.6C69,0.7,75.4,0,81.9,0c612.9,0,1225.8,0,1838.8,0c33.2,0,58,15.7,72.7,49.8
|
||||
c4.5,10.4,6.5,21.7,6.5,33.2c0.1,164.4,0.2,328.8,0,493.2c0,42.1-27.8,75.2-61.1,81.7c-5.8,1.1-11.6,1-17.4,1
|
||||
C1614.7,659,1307.9,659,1001.2,659z M1001.1,645.7c48.3,0,96.6,0,144.9,0c259.2,0,518.5,0,777.7,0c9.5,0,18.7-1.5,27.2-6.2
|
||||
c24.2-13.4,36.5-35.3,36.5-65.8c0-90.8,0-181.5,0-272.3c0-71.8-0.1-143.5,0-215.3c0-18.2-4.5-34.5-15.2-48.3
|
||||
c-13.8-17.6-31.4-24.2-51.9-24.2c-612.8,0.1-1225.5,0.1-1838.3,0.1c-2,0-4,0-6,0.1c-35,1.6-60.8,31.6-60.8,70.9
|
||||
c0,163.3,0,326.6,0,489.8c0,5.5,0.4,10.9,1.5,16.3c6.6,33,31.4,54.8,62.2,54.8C386.3,645.7,693.7,645.7,1001.1,645.7z"/>
|
||||
<path fill="#010101" d="M1000.9,645.7c-307.4,0-614.8,0-922.3,0c-30.8,0-55.6-21.8-62.2-54.8c-1.1-5.3-1.5-10.8-1.5-16.3
|
||||
c0-163.3,0-326.6,0-489.8c0-39.3,25.8-69.4,60.8-70.9c2-0.1,4-0.1,6-0.1c612.8,0,1225.7,0,1838.5-0.1c20.5,0,38.1,6.6,51.9,24.2
|
||||
c10.8,13.8,15.3,30,15.3,48.3c-0.1,71.8,0,143.5,0,215.3c0,90.8,0,181.5,0,272.3c0,30.5-12.3,52.4-36.5,65.8
|
||||
c-8.5,4.7-17.7,6.2-27.2,6.2c-259.3,0-518.6,0-777.8,0C1097.6,645.7,1049.3,645.7,1000.9,645.7z M436.9,292.8
|
||||
c-12.9-7.8-26.1-15.1-39.3-22.2c-0.5-0.4-1-0.9-1.6-1.2c-67.4-37.8-134.7-75.6-202.1-113.4c-5.7-3.2-11.2-6.9-17.7-8.5
|
||||
c-7.9-1.9-15.3-1.6-21.2,5c-6,6.2-6.7,14-6.7,22c0,57.8,0,115.7,0,173.5c0,1.6,0.2,3.3,0.3,4.9c-0.1,11.4-0.3,22.9-0.3,34.3
|
||||
c0,21.4,0.2,42.8,0.2,64.2c0,13.7-0.3,27.4,0,41.1c0.1,6.4,1.9,12.6,6.6,17.4c3.7,4,8.2,5.6,13.6,5.7c6.9,0.1,12.7-2.6,18.5-5.8
|
||||
c19-10.7,38-21.3,57-32c51.1-28.7,102.2-57.5,153.3-86.2c12.5-7,24.9-14.1,37.4-21.1c12.3-6.8,24.6-13.6,36.7-20.6
|
||||
c5.1-2.9,9-7.1,11.1-12.7c3.3-8.5-0.1-17.4-9.7-23.9C461.5,305.6,449.2,299.2,436.9,292.8z M741.4,374.8c-13.5,0-26.9,0.1-40.4-0.1
|
||||
c-3.1,0-4,0.8-3.9,3.9c0.2,5.7,0.3,11.5,0,17.2c-0.2,4,0.8,5.3,5,5.2c17.9-0.3,35.8,0,53.7-0.2c3.3,0,4.1,0.6,3.5,4.1
|
||||
c-2.5,15.2-9.8,27.3-22.6,36c-9.2,6.2-19.2,9.5-30.2,11c-28.7,3.9-57.3-10.8-70.5-36.5c-13.3-26-8.9-58.3,10.9-79.1
|
||||
c25.2-26.6,66-29.1,94.4-5.7c2.6,2.2,4,2.1,6.3-0.3c3.9-4.2,7.9-8.4,12.3-12.1c3.1-2.7,2.5-4.2-0.3-6.5
|
||||
c-28-23.3-59.6-28.7-93.7-17.6c-54.5,17.8-81,77.8-57.4,128.3c15.4,33.1,51.4,58.4,96.1,55.9c48.1-2.7,89-39.9,81.6-99.8
|
||||
c-0.4-3.1-1.5-3.7-4.3-3.6C768.4,374.9,754.9,374.8,741.4,374.8z M1158.9,369.6c-12.5-12.1-27.3-15.8-43.7-12
|
||||
c-24.2,5.6-39.9,21.1-46.1,44.9c-6.2,24.2,1,45.2,19.8,61.6c21,18.4,52.6,19.4,69.6,0.9c0.1-0.1,0.6,0,0.9,0c0,4.9,0.4,9.9-0.1,14.7
|
||||
c-1.4,15.1-9.7,25-23.2,28.3c-16.1,3.9-30.4-2.4-38.6-17.2c-1.5-2.8-2.6-3.4-5.6-2.1c-5,2.4-10.2,4.6-15.4,6.3
|
||||
c-4.6,1.5-4.2,3.5-2.2,6.9c13.1,22.3,32.6,32.8,58.2,30.9c21.9-1.6,38.8-11.5,47.6-32.6c4.3-10.3,5.4-21.2,5.4-32.3
|
||||
c-0.1-25,0-49.9,0-74.9c0-9.9-0.1-19.7,0.1-29.6c0.1-3.1-0.8-4-3.9-3.8c-3.9,0.3-7.9,0.1-11.8,0.1c-9.5,0-9.5,0-10.3,9.2
|
||||
C1159.5,369.2,1159.3,369.3,1158.9,369.6z M1417.9,384.8c0,28.6,0.1,57.2-0.1,85.7c0,3.2,0.8,4.3,4.1,4.1c6.1-0.2,12.2-0.3,18.2,0
|
||||
c4.1,0.2,5.2-1,5.2-5.1c-0.2-19.5,0-39.1-0.2-58.6c0-3.4,0.8-4.6,4.3-4.3c4.6,0.3,9.2,0.1,13.8,0.1c11.3-0.2,22.7,0.9,33.9-1.6
|
||||
c32.2-7.1,51.3-36.3,43.9-67.4c-5.4-22.8-27.5-41-52.9-42c-21.6-0.9-43.4-0.2-65-0.6c-4.6-0.1-5.3,1.4-5.2,5.5
|
||||
C1418,328.6,1417.9,356.7,1417.9,384.8z M1680.2,459.5c0,4.8,0.1,8.2,0,11.6c-0.1,2.2,0.3,3.3,2.9,3.2c6.9-0.2,13.8-0.1,20.7,0
|
||||
c2.1,0,2.9-0.6,2.8-2.8c-0.1-23.1,0.4-46.3-0.4-69.4c-0.6-19.5-10.6-33.6-28.6-41.1c-16-6.7-32.6-7-49-1
|
||||
c-10.3,3.8-18.5,10.4-24,20.1c-1.2,2-1.9,3.5,1.3,4.6c6.3,2.2,12.4,5,18.6,7.6c1.4,0.6,2.6,1.1,3.6-0.8c6.9-13.2,30.2-14.5,42.3-5.9
|
||||
c6.4,4.5,10.1,10.5,9.7,19.1c-2-0.8-3.6-1.6-5.3-2.2c-21.5-7.6-42.1-6-60.7,7.7c-17.3,12.6-19.1,36.8-5.1,53
|
||||
C1623.5,480.1,1661.7,486.8,1680.2,459.5z M993.8,478.3c34.8,0,62-26.8,62-61c0-34.2-27.2-61-62-61c-35.2,0-62.2,26.6-62.1,61.3
|
||||
C931.8,452,958.8,478.3,993.8,478.3z M796.4,417.2c0,34.5,26.8,61,61.8,61c35.1,0,62-26.2,62.1-60.7c0.1-34.7-26.8-61.3-62-61.3
|
||||
C823.3,356.3,796.4,382.7,796.4,417.2z M1276.2,435.8c1.6-0.7,3.3-1.5,4.9-2.1c22.1-9,44.2-18.1,66.3-27.1
|
||||
c11.8-4.8,11.8-4.8,6.2-16.5c-14.7-30.9-48.6-42.7-78.2-27.1c-23.9,12.6-36.4,42-28.9,70.1c6.3,23.6,21.8,38.8,45.7,43.8
|
||||
c25.2,5.2,46.6-2.3,63.1-22.7c1.8-2.2,1.6-3.4-0.9-4.8c-4.6-2.8-9.3-5.6-13.5-8.9c-3.4-2.7-5.1-2.3-8,1.1
|
||||
C1316.5,461.1,1287.6,458.2,1276.2,435.8z M1832,359.8c-9.1,0-17.7,0.2-26.2-0.1c-3.2-0.1-4.4,1.1-5.6,3.9
|
||||
c-9.1,23.1-18.4,46.2-27.7,69.3c-0.6,1.6-0.8,3.5-2.6,4.7c-0.7-1.6-1.4-3-2.1-4.5c-9.6-23.1-19.1-46.3-28.7-69.4
|
||||
c-0.7-1.6-0.6-4-3.5-4c-8.9,0.1-17.9,0-27.3,0c0.8,1.8,1.2,3,1.7,4.2c14.9,33.5,29.8,67,44.8,100.4c1.3,2.9,1.4,5.3,0,8.2
|
||||
c-5.9,12.4-11.5,25-17.2,37.5c-2.8,6.2-5.6,12.4-8.6,19c8.6,0,16.4-0.2,24.3,0.1c3.2,0.1,4.5-1.1,5.7-3.9c9.6-22.1,19.4-44,29.1-66
|
||||
C1802.7,426.3,1817.2,393.4,1832,359.8z M1204.2,384.4c0,28.4,0.1,56.8-0.1,85.2c0,4.2,1.2,5.3,5.2,5.1c5.7-0.3,11.5-0.4,17.2,0
|
||||
c4.2,0.3,5.1-1.1,5.1-5.1c-0.2-44.5-0.1-89-0.1-133.5c0-12.3-0.1-24.6,0.1-36.9c0.1-3.3-0.9-4.2-4.1-4.1c-6.2,0.2-12.5,0.3-18.7,0
|
||||
c-3.7-0.2-4.7,0.9-4.6,4.6C1204.3,327.9,1204.2,356.2,1204.2,384.4z M1584.6,384.9c0-28.4-0.1-56.8,0.1-85.2c0-3.7-0.8-4.8-4.6-4.6
|
||||
c-5.9,0.3-11.8,0.3-17.7,0c-3.9-0.2-4.9,1-4.9,4.9c0.2,25.6,0.1,51.2,0.1,76.8c0,31.2,0.1,62.4-0.1,93.6c0,3.5,0.9,4.5,4.4,4.3
|
||||
c6.1-0.3,12.2-0.3,18.2,0c3.7,0.2,4.6-0.9,4.6-4.6C1584.5,441.7,1584.6,413.3,1584.6,384.9z M671.1,183.1c-4.6,0-9.2,0-13.8,0
|
||||
c-9.4,0-9.2,0-9.3,9.2c0,3.5,1.3,4,4.3,3.9c8-0.2,16.1,0.1,24.1-0.1c2.9-0.1,3.4,1,2.7,3.4c-1.1,4-2.6,7.7-5.4,10.9
|
||||
c-11.7,12.8-33.2,13.5-46.2,1.5c-12.9-11.9-13.9-33.9-2.1-46.7c12.2-13.3,32.9-14.3,45.9-2.2c0.8,0.8,1.6,2.8,3.1,1.4
|
||||
c2.9-2.7,6.8-5.2,7.8-8.5c0.8-2.5-3.6-5-6.3-6.8c-15.3-10.6-36.9-10.7-52.4-0.3c-16.2,10.9-23.8,30-19.5,49
|
||||
c6.1,26.9,33.5,42.3,60.6,34c18.9-5.8,30.7-23,29.4-42.7c-0.3-3.9-1-6.6-6.2-6.1C682.4,183.6,676.7,183.1,671.1,183.1z
|
||||
M1114.3,165.2c1.5,2.3,2.3,3.6,3.1,4.9c11.3,17.9,23,35.5,33.7,53.7c3.8,6.4,7.9,8.9,15.1,8.2c3.4-0.4,4.2-1.1,4.2-4.5
|
||||
c-0.1-25-0.1-49.9-0.1-74.9c0-9.9,0-9.7-10.1-9.8c-3.3,0-4.1,1-4,4.1c0.2,17.9,0.2,35.8,0.2,53.7c0,1.5,0,3.1,0,5.9
|
||||
c-12-18.9-23.6-36.3-34.2-54.4c-4.3-7.4-9-10.4-17.4-9.5c-4,0.4-4.7,1.5-4.6,5.1c0.1,24.6,0.1,49.3,0.1,73.9
|
||||
c0,10.3,0,10.1,10.1,10.2c3.5,0,3.9-1.2,3.9-4.2c-0.1-17.1-0.1-34.2-0.1-51.3C1114.3,173.2,1114.3,169.8,1114.3,165.2z M1041,233.9
|
||||
c26.3,0.1,46.4-19.9,46.5-46.2c0.1-26-20-46.4-45.9-46.6c-26.3-0.2-46.6,19.9-46.7,46.1C994.8,213.6,1014.7,233.7,1041,233.9z
|
||||
M707.1,187.4c0,13,0.2,26-0.1,38.9c-0.1,4.2,0.7,5.9,5.4,5.7c13.1-0.4,26.3-0.1,39.4-0.1c9.3,0,9.1,0,9.3-9.2
|
||||
c0.1-3.6-1.2-4.3-4.5-4.2c-10.5,0.2-21,0-31.5,0.1c-3.1,0.1-4-0.9-3.8-3.9c0.2-5.7,0.2-11.5,0-17.2c-0.1-3,1-3.5,3.6-3.4
|
||||
c7.9,0.2,15.8,0.1,23.7,0.1c8.6,0,8.4,0,8.7-8.8c0.1-3.9-1.1-4.7-4.7-4.6c-9,0.2-18.1,0-27.1,0.1c-3.1,0.1-4.4-0.7-4.2-4
|
||||
c0.3-5.6,0.3-11.2,0-16.7c-0.2-3.3,1.2-3.7,4-3.7c8.7,0.2,17.4,0.1,26.1,0.1c9.9,0,9.7,0,9.8-9.7c0-3.4-1.1-4-4.2-4
|
||||
c-15.3,0.1-30.6,0.2-45.8,0c-3.2,0-4.1,0.9-4.1,4.1C707.2,160.5,707.1,173.9,707.1,187.4z M794,194.1c0,9.2,0,18.4,0,27.6
|
||||
c0,10.4,0,10.2,10.1,10.3c3.5,0,4.3-1.1,4.3-4.4c-0.2-18.7-0.1-37.4,0-56.2c0-4.9-2.1-11.3,0.9-14.1c2.7-2.4,9-0.7,13.8-0.8
|
||||
c9.6,0,9.4,0,9.5-9.4c0.1-3.5-1-4.3-4.4-4.3c-16.1,0.2-32.2,0.1-48.3,0.1c-10.8,0-10.7,0-10.2,10.6c0.1,2.3,0.7,3.1,3.1,3
|
||||
c5.7-0.2,11.5,0.2,17.2-0.1c3.4-0.2,4,1.1,4,4.1C793.9,171.8,794,183,794,194.1z M936.8,194.2c0-11.2,0.2-22.3-0.1-33.5
|
||||
c-0.1-3.6,1-4.5,4.4-4.2c3.6,0.3,7.2,0.1,10.8,0.1c9.5,0,9.3,0,9.5-9.5c0.1-3.7-1.2-4.2-4.5-4.2c-16.3,0.2-32.5,0.1-48.8,0.1
|
||||
c-10.1,0-10,0-9.9,10c0,2.7,0.6,3.8,3.5,3.6c5.4-0.2,10.9,0.2,16.3-0.1c3.7-0.2,5,0.7,4.9,4.7c-0.2,20.2-0.1,40.4-0.1,60.6
|
||||
c0,10.3,0,10.1,10.2,10.2c2.9,0,3.9-0.6,3.9-3.8C936.7,216.9,936.8,205.6,936.8,194.2z M887.3,187.7c0-11.7,0-23.3,0-35
|
||||
c0-9.9,0-9.7-10.1-9.9c-3.5,0-4.3,1.1-4.3,4.4c0.1,24.8,0.1,49.6,0.1,74.4c0,10.6,0,10.4,10.6,10.3c3.1,0,3.8-0.9,3.8-3.9
|
||||
C887.2,214.7,887.3,201.2,887.3,187.7z"/>
|
||||
<path fill="#FBFCFC" d="M741.4,374.8c13.5,0,26.9,0.1,40.4-0.1c2.8,0,3.9,0.6,4.3,3.6c7.5,60-33.5,97.2-81.6,99.8
|
||||
c-44.6,2.5-80.6-22.8-96.1-55.9c-23.6-50.5,2.9-110.5,57.4-128.3c34.1-11.1,65.7-5.7,93.7,17.6c2.8,2.4,3.5,3.8,0.3,6.5
|
||||
c-4.3,3.7-8.4,7.9-12.3,12.1c-2.2,2.4-3.6,2.5-6.3,0.3c-28.4-23.4-69.2-20.9-94.4,5.7c-19.8,20.9-24.2,53.2-10.9,79.1
|
||||
c13.2,25.7,41.9,40.4,70.5,36.5c10.9-1.5,21-4.7,30.2-11c12.8-8.7,20.1-20.8,22.6-36c0.6-3.4-0.3-4.1-3.5-4.1
|
||||
c-17.9,0.1-35.8-0.1-53.7,0.2c-4.2,0.1-5.3-1.2-5-5.2c0.3-5.7,0.3-11.5,0-17.2c-0.1-3,0.8-3.9,3.9-3.9
|
||||
C714.5,374.9,728,374.8,741.4,374.8z"/>
|
||||
<path fill="#65BE69" d="M155,152.5c5.9-6.6,13.4-6.9,21.2-5c6.5,1.6,12,5.3,17.7,8.5c67.4,37.8,134.7,75.6,202.1,113.4
|
||||
c0.6,0.3,1,0.8,1.6,1.2c-3,3.1-6,6.1-9,9.2c-17.6,16.8-35,33.9-52,51.3c-0.3,0.1-0.5,0.1-0.8,0.1c-9.9-11-20.7-21.2-31.7-31
|
||||
c-0.3-0.6-0.5-1.2-0.9-1.7c-16-15.9-31.7-32.2-48.6-47.2c-0.5-0.6-1-1.3-1.5-1.9c-26-26-52-51.9-78.1-77.8
|
||||
C168.4,165.1,161.6,158.9,155,152.5z"/>
|
||||
<path fill="#EF4049" d="M335.8,331.2c0.3,0,0.5,0,0.8-0.1c8.3,8.3,16.7,16.6,25.1,24.9c9,8.8,18,17.6,27,26.4c3,3.1,6,6.1,8.9,9.2
|
||||
c-51.1,28.7-102.2,57.5-153.3,86.2c-19,10.7-38,21.3-57,32c-5.8,3.2-11.6,6-18.5,5.8c-5.4-0.1-9.9-1.7-13.6-5.7
|
||||
c42.8-42.2,85.5-84.4,128.3-126.7C300.9,366,318.3,348.5,335.8,331.2z"/>
|
||||
<path fill="#FDFEFF" d="M1158.9,369.6c0.4-0.4,0.6-0.4,0.6-0.5c0.8-9.2,0.7-9.2,10.3-9.2c3.9,0,7.9,0.2,11.8-0.1
|
||||
c3-0.2,3.9,0.7,3.9,3.8c-0.2,9.9-0.1,19.7-0.1,29.6c0,25,0,49.9,0,74.9c0,11.1-1.1,22-5.4,32.3c-8.8,21.1-25.7,31-47.6,32.6
|
||||
c-25.6,1.8-45.1-8.6-58.2-30.9c-2-3.4-2.4-5.5,2.2-6.9c5.3-1.7,10.4-3.9,15.4-6.3c2.9-1.4,4-0.7,5.6,2.1
|
||||
c8.2,14.9,22.6,21.1,38.6,17.2c13.5-3.3,21.8-13.1,23.2-28.3c0.5-4.9,0.1-9.8,0.1-14.7c-0.3,0-0.7-0.2-0.9,0
|
||||
c-17.1,18.5-48.7,17.5-69.6-0.9c-18.8-16.5-26-37.4-19.8-61.6c6.1-23.8,21.9-39.3,46.1-44.9C1131.6,353.8,1146.4,357.5,1158.9,369.6
|
||||
z M1094.4,418.6c-0.2,2.5,0.4,5.9,1.2,9.2c3.7,14.9,16.7,25.8,31.6,26.5c14.2,0.7,26.9-8.1,32-22.3c3.8-10.6,3.6-21.3-1-31.6
|
||||
c-5.3-11.8-14.2-19.3-27.5-19.9C1109.9,379.6,1094.3,395.7,1094.4,418.6z"/>
|
||||
<path fill="#31C0F1" d="M304.1,300.1c-0.3-0.6-0.5-1.2-0.9-1.7c-16-15.9-31.7-32.2-48.6-47.2c-0.5-0.6-1-1.3-1.5-1.9
|
||||
c-26-26-52-51.9-78.1-77.8c-6.5-6.5-13.3-12.7-19.9-19c-6,6.2-6.7,14-6.7,22c0,57.8,0,115.7,0,173.5c0,1.6,0.2,3.3,0.3,4.9
|
||||
c-0.1,11.4-0.3,22.9-0.3,34.3c0,21.4,0.2,42.8,0.2,64.2c0,13.7-0.3,27.4,0,41.1c0.1,6.4,1.9,12.6,6.6,17.4
|
||||
c42.8-42.2,85.5-84.4,128.3-126.7c17.5-17.3,34.9-34.7,52.3-52.1C325.8,320.2,315.1,310,304.1,300.1z"/>
|
||||
<path fill="#FAFAFA" d="M1417.9,384.8c0-28.1,0.1-56.2-0.1-84.3c0-4.1,0.6-5.6,5.2-5.5c21.7,0.4,43.4-0.2,65,0.6
|
||||
c25.4,1,47.6,19.2,52.9,42c7.3,31.1-11.7,60.4-43.9,67.4c-11.2,2.5-22.6,1.4-33.9,1.6c-4.6,0.1-9.2,0.3-13.8-0.1
|
||||
c-3.5-0.2-4.4,0.9-4.3,4.3c0.2,19.5,0,39.1,0.2,58.6c0,4.1-1.1,5.3-5.2,5.1c-6.1-0.4-12.2-0.3-18.2,0c-3.3,0.1-4.1-0.9-4.1-4.1
|
||||
C1418,442,1417.9,413.4,1417.9,384.8z M1445.2,350.8c0,9.2,0.1,18.4-0.1,27.6c0,2.6,0.7,3.3,3.3,3.3c12.3-0.1,24.6,0.1,37-0.2
|
||||
c16.4-0.4,30-14.3,30.1-30.4c0.1-16.4-13.5-30.4-30.2-30.7c-12.1-0.2-24.3,0.1-36.5-0.2c-3.3-0.1-3.6,1.3-3.6,4
|
||||
C1445.3,333,1445.2,341.9,1445.2,350.8z"/>
|
||||
<path fill="#FDFEFF" d="M1680.2,459.5c-18.5,27.3-56.7,20.6-71.2,3.8c-14-16.3-12.3-40.4,5.1-53c18.6-13.6,39.2-15.2,60.7-7.7
|
||||
c1.7,0.6,3.3,1.3,5.3,2.2c0.4-8.6-3.3-14.5-9.7-19.1c-12.1-8.5-35.3-7.3-42.3,5.9c-1,1.9-2.2,1.4-3.6,0.8
|
||||
c-6.2-2.6-12.3-5.4-18.6-7.6c-3.2-1.2-2.5-2.6-1.3-4.6c5.5-9.7,13.7-16.3,24-20.1c16.4-6.1,32.9-5.7,49,1c18,7.5,28,21.6,28.6,41.1
|
||||
c0.8,23.1,0.3,46.3,0.4,69.4c0,2.2-0.8,2.8-2.8,2.8c-6.9-0.1-13.8-0.1-20.7,0c-2.6,0.1-3-1.1-2.9-3.2
|
||||
C1680.3,467.7,1680.2,464.3,1680.2,459.5z M1657.8,419.9c-8.9,0-16.3,1-23,5.2c-5.8,3.6-8.8,8.9-7.9,15.8c0.8,6.3,5.3,9.6,10.8,11.6
|
||||
c17.6,6.4,37.9-5,42-23.4c0.5-2.4-0.1-3.5-2.3-4.5C1670.8,421.5,1663.9,419.8,1657.8,419.9z"/>
|
||||
<path fill="#FDFEFF" d="M993.8,478.3c-35.1,0-62-26.3-62.1-60.7c-0.1-34.7,26.9-61.3,62.1-61.3c34.8,0,62,26.7,62,61
|
||||
C1055.8,451.5,1028.7,478.2,993.8,478.3z M1028.5,420.5c0-23-14.1-39.2-32.1-40c-16.4-0.8-29.7,7.9-35.1,23
|
||||
c-5.8,16.1-0.9,34.5,11.9,43.9c11.3,8.2,23.6,9.5,36.1,3C1022.4,443.5,1028.1,431.9,1028.5,420.5z"/>
|
||||
<path fill="#FDFEFF" d="M796.4,417.2c0-34.5,26.9-60.9,61.9-61c35.2,0,62.2,26.6,62,61.3c-0.1,34.5-27,60.7-62.1,60.7
|
||||
C823.2,478.2,796.3,451.7,796.4,417.2z M858.3,380.4c-19.7,0-34.8,15.9-34.9,36.7c-0.1,20.9,15.5,37.3,35.1,37.2
|
||||
c19.4-0.1,34.8-16.5,34.7-37.2C893.2,396.4,878,380.4,858.3,380.4z"/>
|
||||
<path fill="#FDFEFF" d="M1276.2,435.8c11.4,22.3,40.3,25.2,56.8,5.6c2.8-3.4,4.6-3.8,8-1.1c4.2,3.3,8.9,6.2,13.5,8.9
|
||||
c2.5,1.5,2.7,2.6,0.9,4.8c-16.5,20.3-37.9,27.9-63.1,22.7c-23.9-5-39.4-20.2-45.7-43.8c-7.5-28.1,5-57.5,28.9-70.1
|
||||
c29.5-15.6,63.5-3.8,78.2,27.1c5.5,11.7,5.5,11.7-6.2,16.5c-22.1,9-44.2,18.1-66.3,27.1C1279.4,434.4,1277.8,435.1,1276.2,435.8z
|
||||
M1271.6,412c-0.2,2-0.4,3.6,3,2.2c16.4-6.9,32.9-13.6,49.4-20.2c2.8-1.1,2.5-2.3,1.1-4.3c-2.9-4-6.8-6.6-11.4-8.2
|
||||
C1294,374.8,1272.2,390.4,1271.6,412z"/>
|
||||
<path fill="#FDFEFF" d="M1832,359.8c-14.8,33.6-29.3,66.5-43.8,99.5c-9.7,22-19.5,44-29.1,66c-1.2,2.8-2.5,4-5.7,3.9
|
||||
c-7.8-0.2-15.7-0.1-24.3-0.1c3-6.6,5.8-12.8,8.6-19c5.7-12.5,11.3-25,17.2-37.5c1.4-2.9,1.3-5.3,0-8.2
|
||||
c-15-33.4-29.9-66.9-44.8-100.4c-0.5-1.2-1-2.4-1.7-4.2c9.4,0,18.3,0.1,27.3,0c2.9,0,2.8,2.4,3.5,4c9.6,23.1,19.2,46.2,28.7,69.4
|
||||
c0.6,1.5,1.3,2.9,2.1,4.5c1.8-1.3,2-3.2,2.6-4.7c9.3-23.1,18.6-46.1,27.7-69.3c1.1-2.9,2.4-4,5.6-3.9
|
||||
C1814.3,360,1822.8,359.8,1832,359.8z"/>
|
||||
<path fill="#FAFAFA" d="M1204.2,384.4c0-28.2,0.1-56.5-0.1-84.7c0-3.7,0.9-4.8,4.6-4.6c6.2,0.3,12.5,0.2,18.7,0
|
||||
c3.2-0.1,4.2,0.8,4.1,4.1c-0.2,12.3-0.1,24.6-0.1,36.9c0,44.5-0.1,89,0.1,133.5c0,4.1-0.9,5.4-5.1,5.1c-5.7-0.4-11.5-0.4-17.2,0
|
||||
c-4,0.2-5.2-0.9-5.2-5.1C1204.3,441.2,1204.2,412.8,1204.2,384.4z"/>
|
||||
<path fill="#FFFFFF" d="M1584.6,384.9c0,28.4-0.1,56.8,0.1,85.2c0,3.7-0.9,4.8-4.6,4.6c-6.1-0.3-12.2-0.3-18.2,0
|
||||
c-3.4,0.2-4.4-0.9-4.4-4.3c0.1-31.2,0.1-62.4,0.1-93.6c0-25.6,0.1-51.2-0.1-76.8c0-3.9,1-5.1,4.9-4.9c5.9,0.3,11.8,0.3,17.7,0
|
||||
c3.8-0.2,4.6,1,4.6,4.6C1584.5,328.1,1584.6,356.5,1584.6,384.9z"/>
|
||||
<path fill="#FAFAFA" d="M671.1,183.1c5.6,0,11.2,0.4,16.7-0.1c5.1-0.5,5.9,2.1,6.2,6.1c1.3,19.7-10.5,36.9-29.4,42.7
|
||||
c-27.1,8.3-54.5-7.1-60.6-34c-4.3-19,3.3-38.1,19.5-49c15.5-10.4,37-10.4,52.4,0.3c2.7,1.8,7.1,4.3,6.3,6.8c-1,3.3-4.9,5.9-7.8,8.5
|
||||
c-1.5,1.4-2.2-0.6-3.1-1.4c-13-12.1-33.7-11.1-45.9,2.2c-11.7,12.8-10.8,34.8,2.1,46.7c13,12,34.5,11.3,46.2-1.5
|
||||
c2.9-3.1,4.3-6.9,5.4-10.9c0.7-2.4,0.2-3.5-2.7-3.4c-8,0.2-16.1,0-24.1,0.1c-3,0.1-4.3-0.4-4.3-3.9c0.1-9.2-0.1-9.2,9.3-9.2
|
||||
C661.9,183.1,666.5,183.1,671.1,183.1z"/>
|
||||
<path fill="#FDFEFF" d="M1114.3,165.2c0,4.5,0,7.9,0,11.3c0,17.1-0.1,34.2,0.1,51.3c0,3-0.4,4.2-3.9,4.2
|
||||
c-10.1-0.1-10.1,0.1-10.1-10.2c0-24.6,0.1-49.3-0.1-73.9c0-3.6,0.6-4.6,4.6-5.1c8.4-0.9,13.1,2.1,17.4,9.5
|
||||
c10.6,18.1,22.2,35.5,34.2,54.4c0-2.8,0-4.4,0-5.9c0-17.9,0-35.8-0.2-53.7c0-3.1,0.7-4.2,4-4.1c10.1,0.1,10.1-0.1,10.1,9.8
|
||||
c0,25-0.1,49.9,0.1,74.9c0,3.3-0.8,4.1-4.2,4.5c-7.3,0.8-11.4-1.8-15.1-8.2c-10.8-18.2-22.4-35.9-33.7-53.7
|
||||
C1116.6,168.8,1115.8,167.5,1114.3,165.2z"/>
|
||||
<path fill="#FAFAFA" d="M1041,233.9c-26.3-0.1-46.2-20.3-46.1-46.7c0.1-26.2,20.5-46.3,46.7-46.1c25.8,0.2,45.9,20.6,45.9,46.6
|
||||
C1087.4,213.9,1067.3,234,1041,233.9z M1073.1,187.6c0-19-13.3-32.9-31.6-32.9c-18.5-0.1-31.9,13.6-32,32.6
|
||||
c-0.1,19.1,13.5,33.4,31.9,33.3C1059.5,220.5,1073,206.5,1073.1,187.6z"/>
|
||||
<path fill="#FAFAFA" d="M707.1,187.4c0-13.5,0.1-26.9-0.1-40.4c0-3.2,0.8-4.1,4.1-4.1c15.3,0.2,30.6,0.2,45.8,0c3.1,0,4.2,0.6,4.2,4
|
||||
c-0.1,9.7,0.1,9.7-9.8,9.7c-8.7,0-17.4,0.1-26.1-0.1c-2.8-0.1-4.1,0.4-4,3.7c0.3,5.6,0.3,11.2,0,16.7c-0.2,3.4,1.1,4.1,4.2,4
|
||||
c9-0.2,18.1,0.1,27.1-0.1c3.6-0.1,4.8,0.7,4.7,4.6c-0.3,8.8-0.1,8.8-8.7,8.8c-7.9,0-15.8,0.1-23.7-0.1c-2.6-0.1-3.8,0.5-3.6,3.4
|
||||
c0.2,5.7,0.2,11.5,0,17.2c-0.1,3,0.8,4,3.8,3.9c10.5-0.2,21,0,31.5-0.1c3.3-0.1,4.6,0.6,4.5,4.2c-0.2,9.2,0,9.2-9.3,9.2
|
||||
c-13.1,0-26.3-0.2-39.4,0.1c-4.8,0.1-5.5-1.5-5.4-5.7C707.3,213.4,707.1,200.4,707.1,187.4z"/>
|
||||
<path fill="#F5F6F6" d="M794,194.1c0-11.2-0.1-22.3,0.1-33.5c0-3.1-0.6-4.3-4-4.1c-5.7,0.3-11.5,0-17.2,0.1c-2.4,0.1-3-0.7-3.1-3
|
||||
C769.3,143,769.2,143,780,143c16.1,0,32.2,0.1,48.3-0.1c3.4,0,4.4,0.8,4.4,4.3c-0.1,9.4,0,9.4-9.5,9.4c-4.7,0-11.1-1.6-13.8,0.8
|
||||
c-3.1,2.8-0.9,9.2-0.9,14.1c-0.1,18.7-0.1,37.4,0,56.2c0,3.4-0.7,4.5-4.3,4.4c-10.1-0.1-10.1,0-10.1-10.3
|
||||
C794,212.5,794,203.3,794,194.1z"/>
|
||||
<path fill="#FBFCFC" d="M936.8,194.2c0,11.3-0.1,22.7,0.1,34c0.1,3.1-0.9,3.8-3.9,3.8c-10.2,0-10.2,0.1-10.2-10.2
|
||||
c0-20.2-0.1-40.4,0.1-60.6c0-4-1.2-4.9-4.9-4.7c-5.4,0.3-10.8-0.1-16.3,0.1c-2.9,0.1-3.5-0.9-3.5-3.6c-0.1-10-0.2-10,9.9-10
|
||||
c16.3,0,32.5,0.1,48.8-0.1c3.2,0,4.6,0.5,4.5,4.2c-0.2,9.5,0,9.5-9.5,9.5c-3.6,0-7.2,0.2-10.8-0.1c-3.4-0.3-4.5,0.7-4.4,4.2
|
||||
C937,171.9,936.8,183.1,936.8,194.2z"/>
|
||||
<path fill="#F5F6F6" d="M887.3,187.7c0,13.5-0.1,26.9,0.1,40.4c0,3-0.7,3.9-3.8,3.9c-10.6,0.1-10.6,0.2-10.6-10.3
|
||||
c0-24.8,0.1-49.6-0.1-74.4c0-3.4,0.8-4.5,4.3-4.4c10.1,0.1,10.1,0,10.1,9.9C887.3,164.4,887.3,176.1,887.3,187.7z"/>
|
||||
<path fill="#FAC013" d="M473.1,313.3c-11.5-7.7-23.9-14.2-36.2-20.6c-12.9-7.8-26.1-15.1-39.3-22.2c-3,3.1-6,6.1-9,9.2
|
||||
c-17.6,16.8-35,33.9-52,51.3c8.3,8.3,16.7,16.6,25.1,24.9c9,8.8,18,17.6,27,26.4c3,3.1,6,6.1,8.9,9.2c12.5-7,24.9-14.1,37.4-21.1
|
||||
c12.3-6.8,24.6-13.6,36.7-20.6c5.1-2.9,9-7.1,11.1-12.7C486,328.7,482.7,319.8,473.1,313.3z"/>
|
||||
<path fill="#030303" d="M1094.4,418.6c0-22.9,15.5-39,36.3-38.1c13.3,0.6,22.2,8.1,27.5,19.9c4.6,10.3,4.8,21,1,31.6
|
||||
c-5,14.2-17.8,22.9-32,22.3c-14.9-0.7-27.9-11.6-31.6-26.5C1094.7,424.5,1094.2,421.1,1094.4,418.6z"/>
|
||||
<path fill="#010101" d="M1445.2,350.8c0-8.9,0.1-17.7-0.1-26.6c0-2.7,0.3-4.1,3.6-4c12.1,0.2,24.3,0,36.5,0.2
|
||||
c16.7,0.3,30.3,14.3,30.2,30.7c-0.1,16.1-13.7,30.1-30.1,30.4c-12.3,0.3-24.6,0-37,0.2c-2.6,0-3.3-0.7-3.3-3.3
|
||||
C1445.3,369.2,1445.2,360,1445.2,350.8z"/>
|
||||
<path fill="#030303" d="M1657.8,419.9c6.1-0.1,13,1.6,19.7,4.6c2.2,1,2.8,2.1,2.3,4.5c-4,18.4-24.4,29.8-42,23.4
|
||||
c-5.5-2-10-5.3-10.8-11.6c-0.9-6.9,2.1-12.2,7.9-15.8C1641.4,420.9,1648.9,419.8,1657.8,419.9z"/>
|
||||
<path fill="#030303" d="M1028.5,420.5c-0.4,11.4-6.1,23-19.2,29.9c-12.5,6.5-24.8,5.2-36.1-3c-12.8-9.3-17.7-27.8-11.9-43.9
|
||||
c5.4-15.1,18.7-23.8,35.1-23C1014.4,381.3,1028.5,397.5,1028.5,420.5z"/>
|
||||
<path fill="#030303" d="M858.3,380.4c19.7,0,34.8,15.9,34.9,36.8c0.1,20.6-15.3,37-34.7,37.2c-19.6,0.1-35.2-16.3-35.1-37.2
|
||||
C823.5,396.3,838.6,380.4,858.3,380.4z"/>
|
||||
<path fill="#030303" d="M1271.6,412c0.6-21.5,22.4-37.1,42.1-30.4c4.6,1.6,8.5,4.2,11.4,8.2c1.4,1.9,1.8,3.1-1.1,4.3
|
||||
c-16.5,6.6-33,13.3-49.4,20.2C1271.2,415.6,1271.4,413.9,1271.6,412z"/>
|
||||
<path fill="#030303" d="M1073.1,187.6c0,18.9-13.6,32.9-31.8,33c-18.4,0-32-14.2-31.9-33.3c0.1-19,13.5-32.6,32-32.6
|
||||
C1059.8,154.7,1073.1,168.5,1073.1,187.6z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 19 KiB |
1
docs/web-app.svg
Normal file
1
docs/web-app.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 12 KiB |
2
monitoring/LICENSE
Normal file
2
monitoring/LICENSE
Normal file
@ -0,0 +1,2 @@
|
||||
Code in this folder and nested folders is licensed under Apache 2.0
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
3
monitoring/README.md
Normal file
3
monitoring/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Monitoring Support
|
||||
|
||||
This directory contains code related to monitoring Tinode server. Only [Prometheus](https://prometheus.io/) is [supported](./prometheus/) at this time.
|
18
monitoring/promexp/README.md
Normal file
18
monitoring/promexp/README.md
Normal file
@ -0,0 +1,18 @@
|
||||
# Prometheus `expvar` Exporter
|
||||
|
||||
This is a [prometheus](https://prometheus.io/) [exporter](https://prometheus.io/docs/instrumenting/exporters/): a service which reads JSON monitoring data exposed by Tinode server using [expvar](https://golang.org/pkg/expvar/) and re-publishes it in [prometheus format](https://prometheus.io/docs/concepts/data_model/).
|
||||
|
||||
## Usage
|
||||
|
||||
Run this service as
|
||||
```
|
||||
./prometheus --tinode_addr=http://localhost:6060/stats/expvar \
|
||||
--namespace=tinode --listen_at=:6222 --metrics_path=/metrics
|
||||
```
|
||||
|
||||
* `tinode_addr` is the address where the Tinode instance publishes `expvar` data to scrape.
|
||||
* `namespace` is a prefix to use for metrics names. If you are monitoring multiple tinode instances you may want to use different namespaces.
|
||||
* `listen_at` is the hostname to bind to for serving the metrics.
|
||||
* `metrics_path` path under which to expose the metrics.
|
||||
|
||||
Once running, configure your Prometheus monitoring installation to collect data from this exporter.
|
212
monitoring/promexp/exporter.go
Normal file
212
monitoring/promexp/exporter.go
Normal file
@ -0,0 +1,212 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
// Exporter collects metrics from a tinode server.
|
||||
type Exporter struct {
|
||||
address string
|
||||
timeout time.Duration
|
||||
namespace string
|
||||
|
||||
up *prometheus.Desc
|
||||
version *prometheus.Desc
|
||||
topicsLive *prometheus.Desc
|
||||
topicsTotal *prometheus.Desc
|
||||
sessionsLive *prometheus.Desc
|
||||
sessionsTotal *prometheus.Desc
|
||||
clusterLeader *prometheus.Desc
|
||||
clusterSize *prometheus.Desc
|
||||
clusterNodesLive *prometheus.Desc
|
||||
malloced *prometheus.Desc
|
||||
}
|
||||
|
||||
var errKeyNotFound = errors.New("key not found")
|
||||
|
||||
// NewExporter returns an initialized exporter.
|
||||
func NewExporter(server, namespace string, timeout time.Duration) *Exporter {
|
||||
return &Exporter{
|
||||
address: server,
|
||||
timeout: timeout,
|
||||
namespace: namespace,
|
||||
up: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, "", "up"),
|
||||
"If tinode instance is reachable.",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
version: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, "", "version"),
|
||||
"The version of this tinode instance.",
|
||||
[]string{"version"},
|
||||
nil,
|
||||
),
|
||||
topicsLive: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, "", "topics_live_count"),
|
||||
"Number of currenly active topics.",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
topicsTotal: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, "", "topics_total"),
|
||||
"Total number of topics used during instance lifetime.",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
sessionsLive: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, "", "sessions_live_count"),
|
||||
"Number of currenly active sessions.",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
sessionsTotal: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, "", "sessions_total"),
|
||||
"Total number of sessions since instance start.",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
clusterLeader: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, "", "cluster_leader"),
|
||||
"If this cluster node is the cluster leader.",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
clusterSize: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, "", "cluster_size"),
|
||||
"Configured number of cluster nodes.",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
clusterNodesLive: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, "", "cluster_nodes_live"),
|
||||
"Number of cluster nodes believed to be live by the current node.",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
malloced: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, "", "malloced_bytes"),
|
||||
"Number of bytes of memory allocated and in use.",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// Describe describes all the metrics exported by the memcached exporter. It
|
||||
// implements prometheus.Collector.
|
||||
func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
|
||||
ch <- e.up
|
||||
ch <- e.version
|
||||
ch <- e.topicsLive
|
||||
ch <- e.topicsTotal
|
||||
ch <- e.sessionsLive
|
||||
ch <- e.sessionsTotal
|
||||
ch <- e.clusterLeader
|
||||
ch <- e.clusterSize
|
||||
ch <- e.clusterNodesLive
|
||||
ch <- e.malloced
|
||||
}
|
||||
|
||||
// Collect fetches statistics from the configured Tinode instance, and
|
||||
// delivers them as Prometheus metrics. It implements prometheus.Collector.
|
||||
func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
|
||||
resp, err := http.Get(e.address)
|
||||
if err != nil {
|
||||
ch <- prometheus.MustNewConstMetric(e.up, prometheus.GaugeValue, 0)
|
||||
log.Println("Failed to connect to server", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
up := float64(1)
|
||||
|
||||
var stats map[string]interface{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&stats)
|
||||
if err != nil {
|
||||
log.Println("Failed to fetch or parse response", err)
|
||||
up = 0
|
||||
} else {
|
||||
if err := e.parseStats(ch, stats); err != nil {
|
||||
up = 0
|
||||
}
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(e.up, prometheus.GaugeValue, up)
|
||||
}
|
||||
|
||||
func (e *Exporter) parseStats(ch chan<- prometheus.Metric, stats map[string]interface{}) error {
|
||||
|
||||
err := firstError(
|
||||
e.parseAndUpdate(ch, e.version, prometheus.GaugeValue, stats, "Version"),
|
||||
e.parseAndUpdate(ch, e.topicsLive, prometheus.GaugeValue, stats, "LiveTopics"),
|
||||
e.parseAndUpdate(ch, e.topicsTotal, prometheus.CounterValue, stats, "TotalTopics"),
|
||||
e.parseAndUpdate(ch, e.sessionsLive, prometheus.GaugeValue, stats, "LiveSessions"),
|
||||
e.parseAndUpdate(ch, e.sessionsTotal, prometheus.CounterValue, stats, "TotalSessions"),
|
||||
e.parseAndUpdate(ch, e.clusterLeader, prometheus.GaugeValue, stats, "ClusterLeader"),
|
||||
e.parseAndUpdate(ch, e.clusterSize, prometheus.GaugeValue, stats, "TotalClusterNodes"),
|
||||
e.parseAndUpdate(ch, e.clusterNodesLive, prometheus.GaugeValue, stats, "LiveClusterNodes"),
|
||||
e.parseAndUpdate(ch, e.malloced, prometheus.GaugeValue, stats, "memstats.Alloc"),
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *Exporter) parseAndUpdate(ch chan<- prometheus.Metric, desc *prometheus.Desc, valueType prometheus.ValueType,
|
||||
stats map[string]interface{}, key string) error {
|
||||
|
||||
v, err := parseNumeric(stats, key)
|
||||
|
||||
if err == errKeyNotFound {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(desc, valueType, v)
|
||||
return nil
|
||||
}
|
||||
|
||||
func firstError(errs ...error) error {
|
||||
for _, v := range errs {
|
||||
if v != nil {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseNumeric(stats map[string]interface{}, path string) (float64, error) {
|
||||
parts := strings.Split(path, ".")
|
||||
var value interface{}
|
||||
var found bool
|
||||
value = stats
|
||||
for i := 0; i < len(parts); i++ {
|
||||
subset, ok := value.(map[string]interface{})
|
||||
if !ok {
|
||||
log.Println("Invalid key path:", path)
|
||||
return 0, errKeyNotFound
|
||||
}
|
||||
value, found = subset[parts[i]]
|
||||
if !found {
|
||||
log.Println("Invalid key path:", path, "(", parts[i], ")")
|
||||
return 0, errKeyNotFound
|
||||
}
|
||||
}
|
||||
|
||||
floatval, ok := value.(float64)
|
||||
if !ok {
|
||||
log.Println("Value at path is not a float64:", path, value)
|
||||
return 0, errKeyNotFound
|
||||
}
|
||||
|
||||
return floatval, nil
|
||||
}
|
64
monitoring/promexp/main.go
Normal file
64
monitoring/promexp/main.go
Normal file
@ -0,0 +1,64 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/prometheus/common/version"
|
||||
)
|
||||
|
||||
type promHTTPLogger struct{}
|
||||
|
||||
func (l promHTTPLogger) Println(v ...interface{}) {
|
||||
log.Println(v...)
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.Printf("Tinode metrics exporter for Prometheus")
|
||||
|
||||
var (
|
||||
tinodeAddr = flag.String("tinode_addr", "http://localhost:6060/stats/expvar", "Address of the Tinode instance to scrape")
|
||||
namespace = flag.String("namespace", "tinode", "Namespace for metrics '<namespace>_...'")
|
||||
listenAt = flag.String("listen_at", ":6222", "Host name and port to serve collected metrics at.")
|
||||
metricsPath = flag.String("metrics_path", "/metrics", "Path under which to expose metrics.")
|
||||
timeout = flag.Int("timeout", 15, "Tinode connection timeout in seconds")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
if *metricsPath == "/" {
|
||||
log.Fatal("Serving metrics from / is not supported")
|
||||
}
|
||||
|
||||
registry := prometheus.NewRegistry()
|
||||
registry.MustRegister(NewExporter(*tinodeAddr, *namespace, time.Duration(*timeout)*time.Second))
|
||||
http.Handle(*metricsPath,
|
||||
promhttp.InstrumentMetricHandler(
|
||||
registry,
|
||||
promhttp.HandlerFor(
|
||||
registry,
|
||||
promhttp.HandlerOpts{
|
||||
ErrorLog: &promHTTPLogger{},
|
||||
Timeout: time.Duration(*timeout) * time.Second,
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// Index page at web root.
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(`<html><head><title>Tinode Exporter</title></head><body>
|
||||
<h1>Tinode Exporter</h1>
|
||||
<p><a href="` + *metricsPath + `">Metrics</a></p>
|
||||
<h2>Build</h2>
|
||||
<pre>` + version.Info() + ` ` + version.BuildContext() + `</pre>
|
||||
</body></html>`))
|
||||
})
|
||||
|
||||
log.Println("Reading Tinode expvar from", *tinodeAddr)
|
||||
log.Printf("Serving metrics at %s%s", *listenAt, *metricsPath)
|
||||
log.Fatalln(http.ListenAndServe(*listenAt, nil))
|
||||
}
|
@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/tinode/chat/server/auth"
|
||||
"github.com/tinode/chat/server/push"
|
||||
rh "github.com/tinode/chat/server/ringhash"
|
||||
"github.com/tinode/chat/server/store/types"
|
||||
)
|
||||
@ -51,6 +52,8 @@ type ClusterNode struct {
|
||||
address string
|
||||
// Name of the node
|
||||
name string
|
||||
// Fingerprint of the node: unique value which changes when the node restarts.
|
||||
fingerprint int64
|
||||
|
||||
// A number of times this node has failed in a row
|
||||
failCount int
|
||||
@ -89,7 +92,7 @@ type ClusterSess struct {
|
||||
Sid string
|
||||
}
|
||||
|
||||
// ClusterReq is a Proxy to Master request message.
|
||||
// ClusterReq is either a Proxy to Master or intra-cluster routing request message.
|
||||
type ClusterReq struct {
|
||||
// Name of the node sending this request
|
||||
Node string
|
||||
@ -99,7 +102,14 @@ type ClusterReq struct {
|
||||
// Cluster is desynchronized.
|
||||
Signature string
|
||||
|
||||
Pkt *ClientComMessage
|
||||
// Fingerprint of the node sending this request.
|
||||
// Fingerprint changes when the node is restarted.
|
||||
Fingerprint int64
|
||||
|
||||
// Client message. Set for C2S requests.
|
||||
CliMsg *ClientComMessage
|
||||
// Message to be routed. Set for route requests.
|
||||
SrvMsg *ServerComMessage
|
||||
|
||||
// Root user may send messages on behalf of other users.
|
||||
OnBehalfOf string
|
||||
@ -112,6 +122,8 @@ type ClusterReq struct {
|
||||
Sess *ClusterSess
|
||||
// True if the original session has disconnected
|
||||
SessGone bool
|
||||
// For {pres} messages, indicates whether to break reply loop.
|
||||
PresWantReply bool
|
||||
}
|
||||
|
||||
// ClusterResp is a Master to Proxy response message.
|
||||
@ -148,6 +160,7 @@ func (n *ClusterNode) reconnect() {
|
||||
n.connected = true
|
||||
n.reconnecting = false
|
||||
n.lock.Unlock()
|
||||
statsInc("LiveClusterNodes", 1)
|
||||
log.Printf("cluster: connection to '%s' established", n.name)
|
||||
return
|
||||
} else if count == 0 {
|
||||
@ -188,6 +201,7 @@ func (n *ClusterNode) call(proc string, msg, resp interface{}) error {
|
||||
if n.connected {
|
||||
n.endpoint.Close()
|
||||
n.connected = false
|
||||
statsInc("LiveClusterNodes", -1)
|
||||
go n.reconnect()
|
||||
}
|
||||
n.lock.Unlock()
|
||||
@ -224,6 +238,7 @@ func (n *ClusterNode) callAsync(proc string, msg, resp interface{}, done chan *r
|
||||
if n.connected {
|
||||
n.endpoint.Close()
|
||||
n.connected = false
|
||||
statsInc("LiveClusterNodes", -1)
|
||||
go n.reconnect()
|
||||
}
|
||||
n.lock.Unlock()
|
||||
@ -244,7 +259,7 @@ func (n *ClusterNode) callAsync(proc string, msg, resp interface{}, done chan *r
|
||||
func (n *ClusterNode) forward(msg *ClusterReq) error {
|
||||
log.Printf("cluster: forwarding request to node '%s'", n.name)
|
||||
msg.Node = globals.cluster.thisNodeName
|
||||
rejected := false
|
||||
var rejected bool
|
||||
err := n.call("Cluster.Master", msg, &rejected)
|
||||
if err == nil && rejected {
|
||||
err = errors.New("cluster: master node out of sync")
|
||||
@ -255,16 +270,25 @@ func (n *ClusterNode) forward(msg *ClusterReq) error {
|
||||
// Master responds to proxy
|
||||
func (n *ClusterNode) respond(msg *ClusterResp) error {
|
||||
log.Printf("cluster: replying to node '%s'", n.name)
|
||||
unused := false
|
||||
var unused bool
|
||||
return n.call("Cluster.Proxy", msg, &unused)
|
||||
}
|
||||
|
||||
// Routes the message within the cluster.
|
||||
func (n *ClusterNode) route(msg *ClusterReq) error {
|
||||
log.Printf("cluster: routing message for topic '%s' to node '%s'", msg.RcptTo, n.name)
|
||||
var unused bool
|
||||
return n.call("Cluster.Route", msg, &unused)
|
||||
}
|
||||
|
||||
// Cluster is the representation of the cluster.
|
||||
type Cluster struct {
|
||||
// Cluster nodes with RPC endpoints (excluding current node).
|
||||
nodes map[string]*ClusterNode
|
||||
// Name of the local node
|
||||
thisNodeName string
|
||||
// Fingerprint of the local node
|
||||
fingerprint int64
|
||||
|
||||
// Resolved address to listed on
|
||||
listenOn string
|
||||
@ -295,15 +319,24 @@ func (c *Cluster) Master(msg *ClusterReq, rejected *bool) error {
|
||||
}
|
||||
} else if msg.Signature == c.ring.Signature() {
|
||||
// This cluster member received a request for a topic it owns.
|
||||
node := globals.cluster.nodes[msg.Node]
|
||||
|
||||
if node == nil {
|
||||
log.Println("cluster: request from an unknown node", msg.Node)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if the remote node has been restarted and if so cleanup stale sessions
|
||||
// which originated at that node.
|
||||
if node.fingerprint == 0 {
|
||||
node.fingerprint = msg.Fingerprint
|
||||
} else if node.fingerprint != msg.Fingerprint {
|
||||
globals.sessionStore.NodeRestarted(node.name, msg.Fingerprint)
|
||||
node.fingerprint = msg.Fingerprint
|
||||
}
|
||||
|
||||
if sess == nil {
|
||||
// If the session is not found, create it.
|
||||
node := globals.cluster.nodes[msg.Node]
|
||||
if node == nil {
|
||||
log.Println("cluster: request from an unknown node", msg.Node)
|
||||
return nil
|
||||
}
|
||||
|
||||
sess, _ = globals.sessionStore.NewSession(node, msg.Sess.Sid)
|
||||
go sess.rpcWriteLoop()
|
||||
}
|
||||
@ -319,9 +352,9 @@ func (c *Cluster) Master(msg *ClusterReq, rejected *bool) error {
|
||||
sess.platf = msg.Sess.Platform
|
||||
|
||||
// Dispatch remote message to a local session.
|
||||
msg.Pkt.from = msg.OnBehalfOf
|
||||
msg.Pkt.authLvl = msg.AuthLvl
|
||||
sess.dispatch(msg.Pkt)
|
||||
msg.CliMsg.from = msg.OnBehalfOf
|
||||
msg.CliMsg.authLvl = msg.AuthLvl
|
||||
sess.dispatch(msg.CliMsg)
|
||||
} else {
|
||||
// Reject the request: wrong signature, cluster is out of sync.
|
||||
*rejected = true
|
||||
@ -348,6 +381,103 @@ func (Cluster) Proxy(msg *ClusterResp, unused *bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Route endpoint receives intra-cluster messages (e.g. pres) destined for the nodes hosting topic.
|
||||
// Called by Hub.route channel consumer.
|
||||
func (c *Cluster) Route(msg *ClusterReq, rejected *bool) error {
|
||||
log.Printf("cluster: node '%s' received route request for topic '%s' from node '%s'", c.thisNodeName, msg.RcptTo, msg.Node)
|
||||
|
||||
*rejected = false
|
||||
if msg.Signature != c.ring.Signature() || msg.SrvMsg == nil {
|
||||
*rejected = true
|
||||
return nil
|
||||
}
|
||||
msg.SrvMsg.rcptto = msg.RcptTo
|
||||
if msg.SrvMsg.Pres != nil && msg.PresWantReply {
|
||||
msg.SrvMsg.Pres.wantReply = true
|
||||
}
|
||||
globals.hub.route <- msg.SrvMsg
|
||||
return nil
|
||||
}
|
||||
|
||||
// User cache & push notifications management. These are calls received by the Master from Proxy.
|
||||
// The Proxy expects no payload to be returned by the master.
|
||||
|
||||
// UserCacheUpdate endpoint receives updates to user's cached values as well as sends push notifications.
|
||||
func (c *Cluster) UserCacheUpdate(msg *UserCacheReq, rejected *bool) error {
|
||||
log.Printf("cluster: node '%s' received user cache update from node '%s'", c.thisNodeName, msg.Node)
|
||||
usersRequestFromCluster(msg)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sends user cache update to user's Master node where the cache actually resides.
|
||||
// The request is extected to contain users who reside at remote nodes only.
|
||||
func (c *Cluster) routeUserReq(req *UserCacheReq) error {
|
||||
// Index requests by cluster node.
|
||||
reqByNode := make(map[string]*UserCacheReq)
|
||||
|
||||
if req.PushRcpt != nil {
|
||||
// Request to send push notifications. Create separate packets for each affected cluster node.
|
||||
for uid, recepient := range req.PushRcpt.To {
|
||||
n := c.nodeForTopic(uid.UserId())
|
||||
if n == nil {
|
||||
return errors.New("attempt to update user at a non-existent node (1)")
|
||||
}
|
||||
r := reqByNode[n.name]
|
||||
if r == nil {
|
||||
r = &UserCacheReq{
|
||||
PushRcpt: &push.Receipt{
|
||||
Payload: req.PushRcpt.Payload,
|
||||
To: make(map[types.Uid]push.Recipient)},
|
||||
Node: c.thisNodeName}
|
||||
}
|
||||
r.PushRcpt.To[uid] = recepient
|
||||
reqByNode[n.name] = r
|
||||
}
|
||||
} else if len(req.UserIdList) > 0 {
|
||||
// Request to add/remove user from cache.
|
||||
for _, uid := range req.UserIdList {
|
||||
n := c.nodeForTopic(uid.UserId())
|
||||
if n == nil {
|
||||
return errors.New("attempt to update user at a non-existent node (2)")
|
||||
}
|
||||
r := reqByNode[n.name]
|
||||
if r == nil {
|
||||
r = &UserCacheReq{Node: c.thisNodeName, Inc: req.Inc}
|
||||
}
|
||||
r.UserIdList = append(r.UserIdList, uid)
|
||||
reqByNode[n.name] = r
|
||||
}
|
||||
}
|
||||
|
||||
if len(reqByNode) > 0 {
|
||||
for nodeName, r := range reqByNode {
|
||||
n := globals.cluster.nodes[nodeName]
|
||||
var rejected bool
|
||||
err := n.call("Cluster.UserCacheUpdate", r, &rejected)
|
||||
if rejected {
|
||||
err = errors.New("cluster: master node out of sync")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update to cached values.
|
||||
n := c.nodeForTopic(req.UserId.UserId())
|
||||
if n == nil {
|
||||
return errors.New("attempt to update user at a non-existent node (3)")
|
||||
}
|
||||
req.Node = c.thisNodeName
|
||||
var rejected bool
|
||||
err := n.call("Cluster.UserCacheUpdate", req, &rejected)
|
||||
if rejected {
|
||||
err = errors.New("cluster: master node out of sync")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Given topic name, find appropriate cluster node to route message to
|
||||
func (c *Cluster) nodeForTopic(topic string) *ClusterNode {
|
||||
key := c.ring.Get(topic)
|
||||
@ -373,6 +503,20 @@ func (c *Cluster) isRemoteTopic(topic string) bool {
|
||||
return c.ring.Get(topic) != c.thisNodeName
|
||||
}
|
||||
|
||||
// Returns remote node name where the topic is hosted.
|
||||
// If the topic is hosted locally, returns an empty string.
|
||||
func (c *Cluster) nodeNameForTopicIfRemote(topic string) string {
|
||||
if c == nil {
|
||||
// Cluster not initialized, all topics are local
|
||||
return ""
|
||||
}
|
||||
key := c.ring.Get(topic)
|
||||
if key == c.thisNodeName {
|
||||
return ""
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
// isPartitioned checks if the cluster is partitioned due to network or other failure and if the
|
||||
// current node is a part of the smaller partition.
|
||||
func (c *Cluster) isPartitioned() bool {
|
||||
@ -384,7 +528,7 @@ func (c *Cluster) isPartitioned() bool {
|
||||
return (len(c.nodes)+1)/2 >= len(c.fo.activeNodes)
|
||||
}
|
||||
|
||||
// Forward client message to the Master (cluster node which owns the topic)
|
||||
// Forward client request message to the Master (cluster node which owns the topic)
|
||||
func (c *Cluster) routeToTopic(msg *ClientComMessage, topic string, sess *Session) error {
|
||||
// Find the cluster node which owns the topic, then forward to it.
|
||||
n := c.nodeForTopic(topic)
|
||||
@ -392,17 +536,17 @@ func (c *Cluster) routeToTopic(msg *ClientComMessage, topic string, sess *Sessio
|
||||
return errors.New("attempt to route to non-existent node")
|
||||
}
|
||||
|
||||
// Save node name: it's need in order to inform relevant nodes when the session is disconnected
|
||||
if sess.nodes == nil {
|
||||
sess.nodes = make(map[string]bool)
|
||||
if sess.getRemoteSub(topic) == nil {
|
||||
log.Printf("Remote subscription missing for topic '%s', sid '%s'", topic, sess.sid)
|
||||
sess.addRemoteSub(topic, &RemoteSubscription{node: n.name})
|
||||
}
|
||||
sess.nodes[n.name] = true
|
||||
|
||||
req := &ClusterReq{
|
||||
Node: c.thisNodeName,
|
||||
Signature: c.ring.Signature(),
|
||||
Pkt: msg,
|
||||
RcptTo: topic,
|
||||
Node: c.thisNodeName,
|
||||
Signature: c.ring.Signature(),
|
||||
Fingerprint: c.fingerprint,
|
||||
CliMsg: msg,
|
||||
RcptTo: topic,
|
||||
Sess: &ClusterSess{
|
||||
Uid: sess.uid,
|
||||
AuthLvl: sess.authLvl,
|
||||
@ -424,26 +568,58 @@ func (c *Cluster) routeToTopic(msg *ClientComMessage, topic string, sess *Sessio
|
||||
|
||||
}
|
||||
|
||||
// Forward server response message to the node that owns topic.
|
||||
func (c *Cluster) routeToTopicIntraCluster(topic string, msg *ServerComMessage) error {
|
||||
n := c.nodeForTopic(topic)
|
||||
if n == nil {
|
||||
return errors.New("attempt to route to non-existent node")
|
||||
}
|
||||
|
||||
req := &ClusterReq{
|
||||
Node: c.thisNodeName,
|
||||
Signature: c.ring.Signature(),
|
||||
Fingerprint: c.fingerprint,
|
||||
RcptTo: topic,
|
||||
SrvMsg: msg}
|
||||
if msg.Pres != nil && msg.Pres.wantReply {
|
||||
req.PresWantReply = true
|
||||
}
|
||||
|
||||
return n.route(req)
|
||||
}
|
||||
|
||||
// Session terminated at origin. Inform remote Master nodes that the session is gone.
|
||||
func (c *Cluster) sessionGone(sess *Session) error {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save node name: it's need in order to inform relevant nodes when the session is disconnected
|
||||
for name := range sess.nodes {
|
||||
n := c.nodes[name]
|
||||
notifiedNodes := make(map[string]bool)
|
||||
|
||||
sess.remoteSubsLock.RLock()
|
||||
defer sess.remoteSubsLock.RUnlock()
|
||||
|
||||
for _, remSub := range sess.remoteSubs {
|
||||
nodeName := remSub.node
|
||||
if notifiedNodes[nodeName] {
|
||||
continue
|
||||
}
|
||||
notifiedNodes[nodeName] = true
|
||||
n := c.nodes[nodeName]
|
||||
if n != nil {
|
||||
return n.forward(
|
||||
if err := n.forward(
|
||||
&ClusterReq{
|
||||
Node: c.thisNodeName,
|
||||
SessGone: true,
|
||||
Node: c.thisNodeName,
|
||||
Fingerprint: c.fingerprint,
|
||||
SessGone: true,
|
||||
Sess: &ClusterSess{
|
||||
Uid: sess.uid,
|
||||
RemoteAddr: sess.remoteAddr,
|
||||
UserAgent: sess.userAgent,
|
||||
Ver: sess.ver,
|
||||
Sid: sess.sid}})
|
||||
Sid: sess.sid}}); err != nil {
|
||||
log.Printf("cluster: remote session shutdown failure: node '%s', error: '%s'", nodeName, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@ -455,6 +631,16 @@ func clusterInit(configString json.RawMessage, self *string) int {
|
||||
log.Fatal("Cluster already initialized.")
|
||||
}
|
||||
|
||||
// Registering variables even if it's a standalone server. Otherwise monitoring software will
|
||||
// complain about missing vars.
|
||||
|
||||
// 1 if this node is cluster leader, 0 otherwise
|
||||
statsRegisterInt("ClusterLeader")
|
||||
// Total number of nodes configured
|
||||
statsRegisterInt("TotalClusterNodes")
|
||||
// Number of nodes currently believed to be up.
|
||||
statsRegisterInt("LiveClusterNodes")
|
||||
|
||||
// This is a standalone server, not initializing
|
||||
if len(configString) == 0 {
|
||||
log.Println("Running as a standalone server.")
|
||||
@ -482,6 +668,7 @@ func clusterInit(configString json.RawMessage, self *string) int {
|
||||
|
||||
globals.cluster = &Cluster{
|
||||
thisNodeName: thisName,
|
||||
fingerprint: time.Now().Unix(),
|
||||
nodes: make(map[string]*ClusterNode)}
|
||||
|
||||
var nodeNames []string
|
||||
@ -512,6 +699,8 @@ func clusterInit(configString json.RawMessage, self *string) int {
|
||||
sort.Strings(nodeNames)
|
||||
workerId := sort.SearchStrings(nodeNames, thisName) + 1
|
||||
|
||||
statsSet("TotalClusterNodes", int64(len(globals.cluster.nodes)+1))
|
||||
|
||||
return workerId
|
||||
}
|
||||
|
||||
@ -631,3 +820,29 @@ func (c *Cluster) rehash(nodes []string) []string {
|
||||
|
||||
return ringKeys
|
||||
}
|
||||
|
||||
// Iterates over sessions hosted on this node and for each session
|
||||
// sends "{pres term}" to all displayed topics.
|
||||
// Called immediately after Cluster.rehash().
|
||||
func (c *Cluster) invalidateRemoteSubs() {
|
||||
globals.sessionStore.lock.Lock()
|
||||
defer globals.sessionStore.lock.Unlock()
|
||||
|
||||
for _, s := range globals.sessionStore.sessCache {
|
||||
if s.proto == CLUSTER || len(s.remoteSubs) == 0 {
|
||||
continue
|
||||
}
|
||||
s.remoteSubsLock.Lock()
|
||||
var topicsToTerminate []string
|
||||
for topic, remSub := range s.remoteSubs {
|
||||
if remSub.node != c.ring.Get(topic) {
|
||||
if remSub.originalTopic != "" {
|
||||
topicsToTerminate = append(topicsToTerminate, remSub.originalTopic)
|
||||
}
|
||||
delete(s.remoteSubs, topic)
|
||||
}
|
||||
}
|
||||
s.remoteSubsLock.Unlock()
|
||||
s.presTermDirect(topicsToTerminate)
|
||||
}
|
||||
}
|
||||
|
@ -179,6 +179,7 @@ func (c *Cluster) sendPings() {
|
||||
|
||||
c.fo.activeNodes = activeNodes
|
||||
c.rehash(activeNodes)
|
||||
c.invalidateRemoteSubs()
|
||||
|
||||
log.Println("cluster: initiating failover rehash for nodes", activeNodes)
|
||||
globals.hub.rehash <- true
|
||||
@ -190,6 +191,9 @@ func (c *Cluster) electLeader() {
|
||||
c.fo.term++
|
||||
c.fo.leader = ""
|
||||
|
||||
// Make sure the current node does not report itself as a leader.
|
||||
statsSet("ClusterLeader", 0)
|
||||
|
||||
log.Println("cluster: leading new election for term", c.fo.term)
|
||||
|
||||
nodeCount := len(c.nodes)
|
||||
@ -236,7 +240,8 @@ func (c *Cluster) electLeader() {
|
||||
if voteCount >= expectVotes {
|
||||
// Current node elected as the leader
|
||||
c.fo.leader = c.thisNodeName
|
||||
log.Println("Elected myself as a new leader")
|
||||
statsSet("ClusterLeader", 1)
|
||||
log.Printf("'%s' elected self as a new leader", c.thisNodeName)
|
||||
}
|
||||
}
|
||||
|
||||
@ -245,6 +250,7 @@ func (c *Cluster) run() {
|
||||
|
||||
ticker := time.NewTicker(c.fo.heartBeat)
|
||||
|
||||
// Count of missed pings from the leader.
|
||||
missed := 0
|
||||
// Don't rehash immediately on the first ping. If this node just came onlyne, leader will
|
||||
// account it on the next ping. Otherwise it will be rehashing twice.
|
||||
@ -257,6 +263,8 @@ func (c *Cluster) run() {
|
||||
// I'm the leader, send pings
|
||||
c.sendPings()
|
||||
} else {
|
||||
// Increment the number of missed pings from the leader.
|
||||
// The counter will be reset to zero when the ping is received.
|
||||
missed++
|
||||
if missed >= c.fo.voteTimeout {
|
||||
// Elect the leader
|
||||
@ -294,6 +302,7 @@ func (c *Cluster) run() {
|
||||
log.Println("cluster: rehashing at a request of",
|
||||
ping.Leader, ping.Nodes, ping.Signature, c.ring.Signature())
|
||||
c.rehash(ping.Nodes)
|
||||
c.invalidateRemoteSubs()
|
||||
rehashSkipped = false
|
||||
|
||||
globals.hub.rehash <- true
|
||||
|
@ -896,11 +896,10 @@ func ErrClusterUnreachable(id, topic string, ts time.Time) *ServerComMessage {
|
||||
}
|
||||
|
||||
// ErrVersionNotSupported invalid (too low) protocol version (505).
|
||||
func ErrVersionNotSupported(id, topic string, ts time.Time) *ServerComMessage {
|
||||
func ErrVersionNotSupported(id string, ts time.Time) *ServerComMessage {
|
||||
return &ServerComMessage{Ctrl: &MsgServerCtrl{
|
||||
Id: id,
|
||||
Code: http.StatusHTTPVersionNotSupported, // 505
|
||||
Text: "version not supported",
|
||||
Topic: topic,
|
||||
Timestamp: ts}}
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ const (
|
||||
defaultDSN = "root:@tcp(localhost:3306)/tinode?parseTime=true"
|
||||
defaultDatabase = "tinode"
|
||||
|
||||
adpVersion = 108
|
||||
adpVersion = 109
|
||||
|
||||
adapterName = "mysql"
|
||||
|
||||
@ -302,6 +302,11 @@ func (a *adapter) CreateDb(reset bool) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create system topic 'sys'.
|
||||
if err = createSystemTopic(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Indexed topic tags.
|
||||
if _, err = tx.Exec(
|
||||
`CREATE TABLE topictags(
|
||||
@ -445,6 +450,14 @@ func (a *adapter) CreateDb(reset bool) error {
|
||||
}
|
||||
|
||||
func (a *adapter) UpgradeDb() error {
|
||||
bumpVersion := func(a *adapter, x int) error {
|
||||
if err := a.updateDbVersion(x); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := a.GetDbVersion()
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := a.GetDbVersion(); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -464,11 +477,7 @@ func (a *adapter) UpgradeDb() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := a.updateDbVersion(107); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := a.GetDbVersion(); err != nil {
|
||||
if err := bumpVersion(a, 107); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -482,11 +491,25 @@ func (a *adapter) UpgradeDb() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := a.updateDbVersion(108); err != nil {
|
||||
if err := bumpVersion(a, 108); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if a.version == 108 {
|
||||
tx, err := a.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = createSystemTopic(tx); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := a.GetDbVersion(); err != nil {
|
||||
if err = bumpVersion(a, 109); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -498,6 +521,14 @@ func (a *adapter) UpgradeDb() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func createSystemTopic(tx *sql.Tx) error {
|
||||
now := t.TimeNow()
|
||||
sql := `INSERT INTO topics(createdat,updatedat,name,access,public)
|
||||
VALUES(?,?,'sys','{"Auth": "N","Anon": "N"}','{"fn": "System"}')`
|
||||
_, err := tx.Exec(sql, now, now)
|
||||
return err
|
||||
}
|
||||
|
||||
func addTags(tx *sqlx.Tx, table, keyName string, keyVal interface{}, tags []string, ignoreDups bool) error {
|
||||
|
||||
if len(tags) == 0 {
|
||||
|
@ -280,6 +280,10 @@ func (a *adapter) CreateDb(reset bool) error {
|
||||
if _, err := rdb.DB(a.dbName).Table("topics").IndexCreate("Tags", rdb.IndexCreateOpts{Multi: true}).RunWrite(a.conn); err != nil {
|
||||
return err
|
||||
}
|
||||
// Create system topic 'sys'.
|
||||
if err := createSystemTopic(a); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Stored message
|
||||
if _, err := rdb.DB(a.dbName).TableCreate("messages", rdb.TableCreateOpts{PrimaryKey: "Id"}).RunWrite(a.conn); err != nil {
|
||||
@ -354,6 +358,14 @@ func (a *adapter) CreateDb(reset bool) error {
|
||||
}
|
||||
|
||||
func (a *adapter) UpgradeDb() error {
|
||||
bumpVersion := func(a *adapter, x int) error {
|
||||
if err := a.updateDbVersion(x); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := a.GetDbVersion()
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := a.GetDbVersion()
|
||||
if err != nil {
|
||||
return err
|
||||
@ -369,11 +381,19 @@ func (a *adapter) UpgradeDb() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := a.updateDbVersion(108); err != nil {
|
||||
if err := bumpVersion(a, 108); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if a.version == 108 {
|
||||
// Perform database upgrade from versions 108 to version 109.
|
||||
|
||||
if err := createSystemTopic(a); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := a.GetDbVersion(); err != nil {
|
||||
if err := bumpVersion(a, 109); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -385,15 +405,24 @@ func (a *adapter) UpgradeDb() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create system topic 'sys'.
|
||||
func createSystemTopic(a *adapter) error {
|
||||
now := t.TimeNow()
|
||||
_, err := rdb.DB(a.dbName).Table("topics").Insert(&t.Topic{
|
||||
ObjHeader: t.ObjHeader{Id: "sys",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now},
|
||||
Access: t.DefaultAccess{Auth: t.ModeNone, Anon: t.ModeNone},
|
||||
Public: map[string]interface{}{"fn": "System"},
|
||||
}).RunWrite(a.conn)
|
||||
return err
|
||||
}
|
||||
|
||||
// UserCreate creates a new user. Returns error and true if error is due to duplicate user name,
|
||||
// false for any other error
|
||||
func (a *adapter) UserCreate(user *t.User) error {
|
||||
_, err := rdb.DB(a.dbName).Table("users").Insert(&user).RunWrite(a.conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
// Add user's authentication record
|
||||
|
@ -29,7 +29,7 @@ import (
|
||||
)
|
||||
|
||||
func listenAndServe(addr string, mux *http.ServeMux, tlfConf *tls.Config, stop <-chan bool) error {
|
||||
shuttingDown := false
|
||||
globals.shuttingDown = false
|
||||
|
||||
httpdone := make(chan bool)
|
||||
|
||||
@ -64,7 +64,7 @@ func listenAndServe(addr string, mux *http.ServeMux, tlfConf *tls.Config, stop <
|
||||
err = server.ListenAndServe()
|
||||
}
|
||||
if err != nil {
|
||||
if shuttingDown {
|
||||
if globals.shuttingDown {
|
||||
log.Println("HTTP server: stopped")
|
||||
} else {
|
||||
log.Println("HTTP server: failed", err)
|
||||
@ -79,7 +79,7 @@ Loop:
|
||||
select {
|
||||
case <-stop:
|
||||
// Flip the flag that we are terminating and close the Accept-ing socket, so no new connections are possible.
|
||||
shuttingDown = true
|
||||
globals.shuttingDown = true
|
||||
// Give server 2 seconds to shut down.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
if err := server.Shutdown(ctx); err != nil {
|
||||
|
@ -21,20 +21,22 @@ import (
|
||||
|
||||
// Request to hub to subscribe session to topic
|
||||
type sessionJoin struct {
|
||||
// Routable (expanded) name of the topic to subscribe to
|
||||
// Routable (expanded) name of the topic to subscribe to.
|
||||
topic string
|
||||
// Packet, containing request details
|
||||
// Packet, containing request details.
|
||||
pkt *ClientComMessage
|
||||
// Session to subscribe
|
||||
// Session to subscribe.
|
||||
sess *Session
|
||||
// True if this topic was just created.
|
||||
// In case of p2p topics, it's true if the other user's subscription was
|
||||
// created (as a part of new topic creation or just alone).
|
||||
created bool
|
||||
// True if the topic was just activated (loaded into memory)
|
||||
// True if the topic was just activated (loaded into memory).
|
||||
loaded bool
|
||||
// True if this is a new subscription
|
||||
// True if this is a new subscription.
|
||||
newsub bool
|
||||
// True if this topic is created internally.
|
||||
internal bool
|
||||
}
|
||||
|
||||
// Session wants to leave the topic
|
||||
@ -101,9 +103,6 @@ type Hub struct {
|
||||
|
||||
// Request to shutdown, unbuffered
|
||||
shutdown chan chan<- bool
|
||||
|
||||
// Flag for indicating that system shutdown is in progress
|
||||
isShutdownInProgress bool
|
||||
}
|
||||
|
||||
func (h *Hub) topicGet(name string) *Topic {
|
||||
@ -138,6 +137,11 @@ func newHub() *Hub {
|
||||
|
||||
go h.run()
|
||||
|
||||
if !globals.cluster.isRemoteTopic("sys") {
|
||||
// Initialize system 'sys' topic. There is only one sys topic per cluster.
|
||||
h.join <- &sessionJoin{topic: "sys", internal: true, pkt: &ClientComMessage{topic: "sys"}}
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
@ -197,6 +201,11 @@ func (h *Hub) run() {
|
||||
log.Println("hub: topic's broadcast queue is full", dst.name)
|
||||
}
|
||||
}
|
||||
} else if (strings.HasPrefix(msg.rcptto, "usr") || strings.HasPrefix(msg.rcptto, "grp")) && globals.cluster.isRemoteTopic(msg.rcptto) {
|
||||
// It is a remote topic.
|
||||
if err := globals.cluster.routeToTopicIntraCluster(msg.rcptto, msg); err != nil {
|
||||
log.Printf("hub: routing to '%s' failed", msg.rcptto)
|
||||
}
|
||||
} else if msg.Pres == nil && msg.Info == nil {
|
||||
// Topic is unknown or offline.
|
||||
// Pres & Info are silently ignored, all other messages are reported as invalid.
|
||||
@ -241,6 +250,8 @@ func (h *Hub) run() {
|
||||
}
|
||||
|
||||
case <-h.rehash:
|
||||
// Cluster rehashing. Some previously local topics became remote.
|
||||
// Such topics must be shut down at this node.
|
||||
h.topics.Range(func(_, t interface{}) bool {
|
||||
topic := t.(*Topic)
|
||||
if globals.cluster.isRemoteTopic(topic.name) {
|
||||
@ -249,10 +260,13 @@ func (h *Hub) run() {
|
||||
return true
|
||||
})
|
||||
|
||||
case hubdone := <-h.shutdown:
|
||||
// mark immediately to prevent more topics being added to hub.topics
|
||||
h.isShutdownInProgress = true
|
||||
// Check if 'sys' topic has migrated to this node.
|
||||
if h.topicGet("sys") == nil && !globals.cluster.isRemoteTopic("sys") {
|
||||
// Yes, 'sys' has migrated here. Initialize it.
|
||||
h.join <- &sessionJoin{topic: "sys", internal: true, pkt: &ClientComMessage{topic: "sys"}}
|
||||
}
|
||||
|
||||
case hubdone := <-h.shutdown:
|
||||
// start cleanup process
|
||||
topicsdone := make(chan bool)
|
||||
topicCount := 0
|
||||
@ -432,7 +446,7 @@ func (h *Hub) topicUnreg(sess *Session, topic string, msg *ClientComMessage, rea
|
||||
statsInc("LiveTopics", -1)
|
||||
}
|
||||
|
||||
// sess && msg could be nil if the topic is being killed by timer
|
||||
// sess && msg could be nil if the topic is being killed by timer or due to rehashing.
|
||||
if sess != nil && msg != nil {
|
||||
sess.queueOut(NoErr(msg.id, msg.topic, now))
|
||||
}
|
||||
|
@ -38,6 +38,9 @@ func topicInit(t *Topic, sreg *sessionJoin, h *Hub) {
|
||||
case strings.HasPrefix(t.xoriginal, "grp"):
|
||||
// Load existing group topic
|
||||
err = initTopicGrp(t, sreg)
|
||||
case t.xoriginal == "sys":
|
||||
// Initialize system topic.
|
||||
err = initTopicSys(t, sreg)
|
||||
default:
|
||||
// Unrecognized topic name
|
||||
err = types.ErrTopicNotFound
|
||||
@ -79,7 +82,7 @@ func topicInit(t *Topic, sreg *sessionJoin, h *Hub) {
|
||||
}
|
||||
|
||||
// prevent newly initialized topics to go live while shutdown in progress
|
||||
if h.isShutdownInProgress {
|
||||
if globals.shuttingDown {
|
||||
h.topicDel(sreg.topic)
|
||||
return
|
||||
}
|
||||
@ -96,7 +99,9 @@ func topicInit(t *Topic, sreg *sessionJoin, h *Hub) {
|
||||
sreg.loaded = true
|
||||
|
||||
// Topic will check access rights, send invite to p2p user, send {ctrl} message to the initiator session
|
||||
t.reg <- sreg
|
||||
if !sreg.internal {
|
||||
t.reg <- sreg
|
||||
}
|
||||
|
||||
t.resume()
|
||||
go t.run(h)
|
||||
@ -587,3 +592,36 @@ func initTopicGrp(t *Topic, sreg *sessionJoin) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Initialize system topic. System topic is a singleton, always in memory.
|
||||
func initTopicSys(t *Topic, sreg *sessionJoin) error {
|
||||
t.cat = types.TopicCatSys
|
||||
|
||||
stopic, err := store.Topics.Get(t.name)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if stopic == nil {
|
||||
return types.ErrTopicNotFound
|
||||
}
|
||||
|
||||
if err = t.loadSubscribers(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// There is no t.owner
|
||||
|
||||
// Default permissions are 'W'
|
||||
t.accessAuth = types.ModeWrite
|
||||
t.accessAnon = types.ModeWrite
|
||||
|
||||
t.public = stopic.Public
|
||||
|
||||
t.created = stopic.CreatedAt
|
||||
t.updated = stopic.UpdatedAt
|
||||
if stopic.TouchedAt != nil {
|
||||
t.touched = *stopic.TouchedAt
|
||||
}
|
||||
t.lastID = stopic.SeqId
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -100,6 +100,8 @@ const (
|
||||
// For instance, to define the buildstamp as a timestamp of when the server was built add a
|
||||
// flag to compiler command line:
|
||||
// -ldflags "-X main.buildstamp=`date -u '+%Y%m%dT%H:%M:%SZ'`"
|
||||
// or to set it to git tag:
|
||||
// -ldflags "-X main.buildstamp=`git describe --tags`"
|
||||
var buildstamp = "undef"
|
||||
|
||||
// CredValidator holds additional config params for a credential validator.
|
||||
@ -112,6 +114,8 @@ type credValidator struct {
|
||||
var globals struct {
|
||||
// Topics cache and processing.
|
||||
hub *Hub
|
||||
// Indicator that shutdown is in progress
|
||||
shuttingDown bool
|
||||
// Sessions cache.
|
||||
sessionStore *SessionStore
|
||||
// Cluster data.
|
||||
@ -123,7 +127,7 @@ var globals struct {
|
||||
// Runtime statistics communication channel.
|
||||
statsUpdate chan *varUpdate
|
||||
// Users cache communication channel.
|
||||
usersUpdate chan *userUpdate
|
||||
usersUpdate chan *UserCacheReq
|
||||
|
||||
// Credential validators.
|
||||
validators map[string]credValidator
|
||||
@ -257,6 +261,17 @@ func main() {
|
||||
config.Listen = *listenOn
|
||||
}
|
||||
|
||||
// Set up HTTP server. Must use non-default mux because of expvar.
|
||||
mux := http.NewServeMux()
|
||||
|
||||
evpath := *expvarPath
|
||||
if evpath == "" {
|
||||
evpath = config.ExpvarPath
|
||||
}
|
||||
statsInit(mux, evpath)
|
||||
statsRegisterInt("Version")
|
||||
statsSet("Version", int64(parseVersion(currentVersion)))
|
||||
|
||||
// Initialize cluster and receive calculated workerId.
|
||||
// Cluster won't be started here yet.
|
||||
workerId := clusterInit(config.Cluster, clusterSelf)
|
||||
@ -283,7 +298,7 @@ func main() {
|
||||
log.Printf("Profiling info saved to '%s.(cpu|mem)'", *pprofFile)
|
||||
}
|
||||
|
||||
err := store.Open(workerId, string(config.Store))
|
||||
err := store.Open(workerId, config.Store)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to connect to DB: ", err)
|
||||
}
|
||||
@ -477,9 +492,6 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Set up HTTP server. Must use non-default mux because of expvar.
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Serve static content from the directory in -static_data flag if that's
|
||||
// available, otherwise assume '<path-to-executable>/static'. The content is served at
|
||||
// the path pointed by 'static_mount' in the config. If that is missing then it's
|
||||
@ -537,12 +549,6 @@ func main() {
|
||||
mux.HandleFunc("/", serve404)
|
||||
}
|
||||
|
||||
evpath := *expvarPath
|
||||
if evpath == "" {
|
||||
evpath = config.ExpvarPath
|
||||
}
|
||||
statsInit(mux, evpath)
|
||||
|
||||
if err = listenAndServe(config.Listen, mux, tlsConfig, signalHandler()); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -295,6 +295,17 @@ func (t *Topic) presSubsOnlineDirect(what string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Communicates "topic unaccessible (cluster rehashing or node connection lost)" event
|
||||
// to a list of topics promting the client to resubscribe to the topics.
|
||||
func (s *Session) presTermDirect(subs []string) {
|
||||
log.Printf("sid '%s', uid '%s', terminating %s", s.sid, s.uid, subs)
|
||||
msg := &ServerComMessage{Pres: &MsgServerPres{Topic: "me", What: "term"}}
|
||||
for _, topic := range subs {
|
||||
msg.Pres.Src = topic
|
||||
s.queueOut(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Publish to topic subscribers's sessions currently offline in the topic, on their 'me'
|
||||
// Group and P2P.
|
||||
// Case E: topic came online, "on"
|
||||
|
@ -3,36 +3,48 @@
|
||||
# Start/stop test cluster on localhost. This is NOT a production script. Use it for reference only.
|
||||
|
||||
# Names of cluster nodes
|
||||
node_names=( one two three )
|
||||
ALL_NODE_NAMES=( one two three )
|
||||
# Port where the first node will listen for client connections over http
|
||||
base_http_port=6080
|
||||
# Port where the first node will listen for gRPC connections.
|
||||
base_grpc_port=6090
|
||||
HTTP_BASE_PORT=6080
|
||||
# Port where the first node will listen for gRPC intra-cluster connections.
|
||||
GRPC_BASE_PORT=6090
|
||||
|
||||
# Allow for non-default config file to be specifid on the command line like config=file_name
|
||||
if [ ! -z "$config" ] ; then
|
||||
TINODE_CONF=$config
|
||||
else
|
||||
TINODE_CONF="./tinode.conf"
|
||||
fi
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
echo 'Running cluster on localhost, ports 6080-6082'
|
||||
|
||||
http_port=$base_http_port
|
||||
grpc_port=$base_grpc_port
|
||||
for node_name in "${node_names[@]}"
|
||||
HTTP_PORT=$HTTP_BASE_PORT
|
||||
GRPC_PORT=$GRPC_BASE_PORT
|
||||
for NODE_NAME in "${ALL_NODE_NAMES[@]}"
|
||||
do
|
||||
./server -config=./tinode.conf -cluster_self=$node_name -listen=:${http_port} -grpc_listen=:${grpc_port} &
|
||||
# /var/tmp/ does not requre root access
|
||||
echo $!> "/var/tmp/tinode-${node_name}.pid"
|
||||
http_port=$((http_port+1))
|
||||
grpc_port=$((grpc_port+1))
|
||||
# Start the node
|
||||
./server -config=${TINODE_CONF} -cluster_self=$NODE_NAME -listen=:${HTTP_PORT} -grpc_listen=:${GRPC_PORT} &
|
||||
# Save PID of the node to a temp file.
|
||||
# /var/tmp/ does not requre root access.
|
||||
echo $!> "/var/tmp/tinode-${NODE_NAME}.pid"
|
||||
# Increment ports for the next node.
|
||||
HTTP_PORT=$((HTTP_PORT+1))
|
||||
GRPC_PORT=$((GRPC_PORT+1))
|
||||
done
|
||||
;;
|
||||
stop)
|
||||
echo 'Stopping cluster'
|
||||
|
||||
for node_name in "${node_names[@]}"
|
||||
for NODE_NAME in "${ALL_NODE_NAMES[@]}"
|
||||
do
|
||||
kill `cat /var/tmp/tinode-${node_name}.pid`
|
||||
rm "/var/tmp/tinode-${node_name}.pid"
|
||||
# Reda PIDs of running nodes from temp files and kill them.
|
||||
kill `cat /var/tmp/tinode-${NODE_NAME}.pid`
|
||||
# Clean up: delete temp files.
|
||||
rm "/var/tmp/tinode-${NODE_NAME}.pid"
|
||||
done
|
||||
;;
|
||||
*)
|
||||
echo $"Usage: $0 {start|stop}"
|
||||
echo $"Usage: $0 {start|stop} [ config=<path_to_tinode.conf> ]"
|
||||
esac
|
||||
|
@ -39,6 +39,14 @@ const sendTimeout = time.Microsecond * 150
|
||||
|
||||
var minSupportedVersionValue = parseVersion(minSupportedVersion)
|
||||
|
||||
// Holds metadata on the subscription/topic hosted on a remote node.
|
||||
type RemoteSubscription struct {
|
||||
// Hosting node.
|
||||
node string
|
||||
// Topic name, as specified by the client.
|
||||
originalTopic string
|
||||
}
|
||||
|
||||
// Session represents a single WS connection or a long polling session. A user may have multiple
|
||||
// sessions.
|
||||
type Session struct {
|
||||
@ -103,8 +111,10 @@ type Session struct {
|
||||
// subs concurrently.
|
||||
subsLock sync.RWMutex
|
||||
|
||||
// Cluster nodes to inform when the session is disconnected
|
||||
nodes map[string]bool
|
||||
// Map of remote topic subscriptions, indexed by topic name.
|
||||
remoteSubs map[string]*RemoteSubscription
|
||||
// Synchronizes access to remoteSubs.
|
||||
remoteSubsLock sync.RWMutex
|
||||
|
||||
// Session ID
|
||||
sid string
|
||||
@ -162,6 +172,27 @@ func (s *Session) unsubAll() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) getRemoteSub(topic string) *RemoteSubscription {
|
||||
s.remoteSubsLock.RLock()
|
||||
defer s.remoteSubsLock.RUnlock()
|
||||
|
||||
return s.remoteSubs[topic]
|
||||
}
|
||||
|
||||
func (s *Session) addRemoteSub(topic string, remoteSub *RemoteSubscription) {
|
||||
s.remoteSubsLock.Lock()
|
||||
defer s.remoteSubsLock.Unlock()
|
||||
|
||||
s.remoteSubs[topic] = remoteSub
|
||||
}
|
||||
|
||||
func (s *Session) delRemoteSub(topic string) {
|
||||
s.remoteSubsLock.Lock()
|
||||
defer s.remoteSubsLock.Unlock()
|
||||
|
||||
delete(s.remoteSubs, topic)
|
||||
}
|
||||
|
||||
// queueOut attempts to send a ServerComMessage to a session; if the send buffer is full,
|
||||
// timeout is `sendTimeout`.
|
||||
func (s *Session) queueOut(msg *ServerComMessage) bool {
|
||||
@ -361,9 +392,11 @@ func (s *Session) dispatch(msg *ClientComMessage) {
|
||||
// Request to subscribe to a topic
|
||||
func (s *Session) subscribe(msg *ClientComMessage) {
|
||||
var expanded string
|
||||
isNewTopic := false
|
||||
if strings.HasPrefix(msg.topic, "new") {
|
||||
// Request to create a new named topic
|
||||
expanded = genTopicName()
|
||||
isNewTopic = true
|
||||
// msg.topic = expanded
|
||||
} else {
|
||||
var resp *ServerComMessage
|
||||
@ -376,11 +409,20 @@ func (s *Session) subscribe(msg *ClientComMessage) {
|
||||
|
||||
if sub := s.getSub(expanded); sub != nil {
|
||||
s.queueOut(InfoAlreadySubscribed(msg.id, msg.topic, msg.timestamp))
|
||||
} else if globals.cluster.isRemoteTopic(expanded) {
|
||||
} else if remoteNodeName := globals.cluster.nodeNameForTopicIfRemote(expanded); remoteNodeName != "" {
|
||||
// The topic is handled by a remote node. Forward message to it.
|
||||
if err := globals.cluster.routeToTopic(msg, expanded, s); err != nil {
|
||||
log.Println("s.subscribe:", err, s.sid)
|
||||
s.queueOut(ErrClusterUnreachable(msg.id, msg.topic, msg.timestamp))
|
||||
} else {
|
||||
var originalTopic string
|
||||
if isNewTopic {
|
||||
// New topics are "grp". It's okay to use expaneded.
|
||||
originalTopic = expanded
|
||||
} else {
|
||||
originalTopic = msg.topic
|
||||
}
|
||||
s.addRemoteSub(expanded, &RemoteSubscription{node: remoteNodeName, originalTopic: originalTopic})
|
||||
}
|
||||
} else {
|
||||
globals.hub.join <- &sessionJoin{
|
||||
@ -420,6 +462,8 @@ func (s *Session) leave(msg *ClientComMessage) {
|
||||
if err := globals.cluster.routeToTopic(msg, expanded, s); err != nil {
|
||||
log.Println("s.leave:", err, s.sid)
|
||||
s.queueOut(ErrClusterUnreachable(msg.id, msg.topic, msg.timestamp))
|
||||
} else {
|
||||
s.delRemoteSub(expanded)
|
||||
}
|
||||
} else if !msg.Leave.Unsub {
|
||||
// Session is not attached to the topic, wants to leave - fine, no change
|
||||
@ -480,6 +524,9 @@ func (s *Session) publish(msg *ClientComMessage) {
|
||||
log.Println("s.publish:", err, s.sid)
|
||||
s.queueOut(ErrClusterUnreachable(msg.id, msg.topic, msg.timestamp))
|
||||
}
|
||||
} else if expanded == "sys" {
|
||||
// Publishing to "sys" topic requires no subsription.
|
||||
globals.hub.route <- data
|
||||
} else {
|
||||
// Publish request received without attaching to topic first.
|
||||
s.queueOut(ErrAttachFirst(msg.id, msg.topic, msg.timestamp))
|
||||
@ -490,6 +537,7 @@ func (s *Session) publish(msg *ClientComMessage) {
|
||||
// Client metadata
|
||||
func (s *Session) hello(msg *ClientComMessage) {
|
||||
var params map[string]interface{}
|
||||
var deviceIDUpdate bool
|
||||
|
||||
if s.ver == 0 {
|
||||
s.ver = parseVersion(msg.Hi.Version)
|
||||
@ -501,11 +549,19 @@ func (s *Session) hello(msg *ClientComMessage) {
|
||||
// Check version compatibility
|
||||
if versionCompare(s.ver, minSupportedVersionValue) < 0 {
|
||||
s.ver = 0
|
||||
s.queueOut(ErrVersionNotSupported(msg.id, "", msg.timestamp))
|
||||
s.queueOut(ErrVersionNotSupported(msg.id, msg.timestamp))
|
||||
log.Println("s.hello:", "unsupported version", s.sid)
|
||||
return
|
||||
}
|
||||
params = map[string]interface{}{"ver": currentVersion, "build": store.GetAdapterName() + ":" + buildstamp}
|
||||
|
||||
params = map[string]interface{}{
|
||||
"ver": currentVersion,
|
||||
"build": store.GetAdapterName() + ":" + buildstamp,
|
||||
"maxMessageSize": globals.maxMessageSize,
|
||||
"maxSubscriberCount": globals.maxSubscriberCount,
|
||||
"maxTagCount": globals.maxTagCount,
|
||||
"maxFileUploadSize": globals.maxFileUploadSize,
|
||||
}
|
||||
|
||||
// Set ua & platform in the beginning of the session.
|
||||
// Don't change them later.
|
||||
@ -515,6 +571,8 @@ func (s *Session) hello(msg *ClientComMessage) {
|
||||
s.platf = platformFromUA(msg.Hi.UserAgent)
|
||||
}
|
||||
} else if msg.Hi.Version == "" || parseVersion(msg.Hi.Version) == s.ver {
|
||||
deviceIDUpdate = true
|
||||
|
||||
// Save changed device ID or Lang. Platform cannot be changed.
|
||||
if !s.uid.IsZero() {
|
||||
if err := store.Devices.Update(s.uid, s.deviceID, &types.DeviceDef{
|
||||
@ -540,8 +598,9 @@ func (s *Session) hello(msg *ClientComMessage) {
|
||||
|
||||
var httpStatus int
|
||||
var httpStatusText string
|
||||
if s.proto == LPOLL {
|
||||
if s.proto == LPOLL || deviceIDUpdate {
|
||||
// In case of long polling StatusCreated was reported earlier.
|
||||
// In case of deviceID update just report success.
|
||||
httpStatus = http.StatusOK
|
||||
httpStatusText = "ok"
|
||||
|
||||
|
@ -63,6 +63,9 @@ func (ss *SessionStore) NewSession(conn interface{}, sid string) (*Session, int)
|
||||
s.send = make(chan interface{}, 256) // buffered
|
||||
s.stop = make(chan interface{}, 1) // Buffered by 1 just to make it non-blocking
|
||||
s.detach = make(chan string, 64) // buffered
|
||||
if globals.cluster != nil {
|
||||
s.remoteSubs = make(map[string]*RemoteSubscription)
|
||||
}
|
||||
}
|
||||
|
||||
s.lastTouched = time.Now()
|
||||
@ -172,6 +175,26 @@ func (ss *SessionStore) EvictUser(uid types.Uid, skipSid string) {
|
||||
statsSet("LiveSessions", int64(len(ss.sessCache)))
|
||||
}
|
||||
|
||||
// NodeRestarted removes stale sessions from a restarted cluster node.
|
||||
// - nodeName is the name of affected node
|
||||
// - fingerprint is the new fingerprint of the node.
|
||||
func (ss *SessionStore) NodeRestarted(nodeName string, fingerprint int64) {
|
||||
ss.lock.Lock()
|
||||
defer ss.lock.Unlock()
|
||||
|
||||
for _, s := range ss.sessCache {
|
||||
if s.proto != CLUSTER || s.clnode.name != nodeName {
|
||||
continue
|
||||
}
|
||||
if s.clnode.fingerprint != fingerprint {
|
||||
s.stop <- nil
|
||||
delete(ss.sessCache, s.sid)
|
||||
}
|
||||
}
|
||||
|
||||
statsSet("LiveSessions", int64(len(ss.sessCache)))
|
||||
}
|
||||
|
||||
// NewSessionStore initializes a session store.
|
||||
func NewSessionStore(lifetime time.Duration) *SessionStore {
|
||||
ss := &SessionStore{
|
||||
|
@ -29,10 +29,10 @@ type configType struct {
|
||||
Adapters map[string]json.RawMessage `json:"adapters"`
|
||||
}
|
||||
|
||||
func openAdapter(workerId int, jsonconf string) error {
|
||||
func openAdapter(workerId int, jsonconf json.RawMessage) error {
|
||||
var config configType
|
||||
if err := json.Unmarshal([]byte(jsonconf), &config); err != nil {
|
||||
return errors.New("store: failed to parse config: " + err.Error() + "(" + jsonconf + ")")
|
||||
if err := json.Unmarshal(jsonconf, &config); err != nil {
|
||||
return errors.New("store: failed to parse config: " + err.Error() + "(" + string(jsonconf) + ")")
|
||||
}
|
||||
|
||||
if adp == nil {
|
||||
@ -67,7 +67,7 @@ func openAdapter(workerId int, jsonconf string) error {
|
||||
// Open initializes the persistence system. Adapter holds a connection pool for a database instance.
|
||||
// name - name of the adapter rquested in the config file
|
||||
// jsonconf - configuration string
|
||||
func Open(workerId int, jsonconf string) error {
|
||||
func Open(workerId int, jsonconf json.RawMessage) error {
|
||||
if err := openAdapter(workerId, jsonconf); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -125,7 +125,7 @@ func GetDbVersion() int {
|
||||
// attempt to drop an existing database. If jsconf is nil it will assume that the adapter is
|
||||
// already open. If it's non-nil and the adapter is not open, it will use the config string
|
||||
// to open the adapter first.
|
||||
func InitDb(jsonconf string, reset bool) error {
|
||||
func InitDb(jsonconf json.RawMessage, reset bool) error {
|
||||
if !IsOpen() {
|
||||
if err := openAdapter(1, jsonconf); err != nil {
|
||||
return err
|
||||
@ -137,7 +137,7 @@ func InitDb(jsonconf string, reset bool) error {
|
||||
// UpgradeDb performes an upgrade of the database to the current adapter version.
|
||||
// If jsconf is nil it will assume that the adapter is already open. If it's non-nil and the
|
||||
// adapter is not open, it will use the config string to open the adapter first.
|
||||
func UpgradeDb(jsonconf string) error {
|
||||
func UpgradeDb(jsonconf json.RawMessage) error {
|
||||
if !IsOpen() {
|
||||
if err := openAdapter(1, jsonconf); err != nil {
|
||||
return err
|
||||
|
@ -81,9 +81,9 @@ func (uid Uid) Compare(u2 Uid) int {
|
||||
}
|
||||
|
||||
// MarshalBinary converts Uid to byte slice.
|
||||
func (uid *Uid) MarshalBinary() ([]byte, error) {
|
||||
func (uid Uid) MarshalBinary() ([]byte, error) {
|
||||
dst := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(dst, uint64(*uid))
|
||||
binary.LittleEndian.PutUint64(dst, uint64(uid))
|
||||
return dst, nil
|
||||
}
|
||||
|
||||
@ -358,6 +358,9 @@ type StringSlice []string
|
||||
|
||||
// Scan implements sql.Scanner interface.
|
||||
func (ss *StringSlice) Scan(val interface{}) error {
|
||||
if val == nil {
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(val.([]byte), ss)
|
||||
}
|
||||
|
||||
@ -422,6 +425,8 @@ const (
|
||||
ModeCAuth AccessMode = ModeCP2P | ModeCPublic
|
||||
// Read-only access to topic ("JR", 3)
|
||||
ModeCReadOnly = ModeJoin | ModeRead
|
||||
// Access to 'sys' topic by a root user ("JRWPD", 79, 0x4F)
|
||||
ModeCSys = ModeJoin | ModeRead | ModeWrite | ModePres | ModeDelete
|
||||
|
||||
// Admin: user who can modify access mode ("OA", dec: 144, hex: 0x90)
|
||||
ModeCAdmin = ModeOwner | ModeApprove
|
||||
@ -1021,6 +1026,8 @@ const (
|
||||
TopicCatP2P
|
||||
// TopicCatGrp is a a value denoting group topic.
|
||||
TopicCatGrp
|
||||
// TopicCatSys is a constant indicating a system topic.
|
||||
TopicCatSys
|
||||
)
|
||||
|
||||
// GetTopicCat given topic name returns topic category.
|
||||
@ -1034,6 +1041,8 @@ func GetTopicCat(name string) TopicCat {
|
||||
return TopicCatGrp
|
||||
case "fnd":
|
||||
return TopicCatFnd
|
||||
case "sys":
|
||||
return TopicCatSys
|
||||
default:
|
||||
panic("invalid topic type for name '" + name + "'")
|
||||
}
|
||||
|
@ -205,10 +205,14 @@
|
||||
// SMTP port to use. "25" for basic email RFC 5321 (2821, 821), "587" for RFC 3207 (TLS).
|
||||
"smtp_port": "25",
|
||||
|
||||
// Address to use for authentication and to show in From:
|
||||
"sender": "noreply@example.com",
|
||||
// RFC 5322 email address to show in the From: field and optionally to use for authentication.
|
||||
// Only 'addr-spec' of RFC 5322 string will be used for authentication.
|
||||
"sender": "\"Tinode\" <noreply@example.com>",
|
||||
|
||||
// Password of the sender.
|
||||
// Optional login to use for authentication; if missing, "sender" will be used.
|
||||
"login": "john.doe@example.com",
|
||||
|
||||
// Password to use when authenticating the sender.
|
||||
"sender_password": "your-password-here",
|
||||
|
||||
// Message body template for credential validation. Uses http/template syntax.
|
||||
|
@ -160,12 +160,12 @@ var nilPresParams = &presParams{}
|
||||
var nilPresFilters = &presFilters{}
|
||||
|
||||
func (t *Topic) run(hub *Hub) {
|
||||
// TODO(gene): read keepalive value from the command line
|
||||
// Kills topic after a period of inactivity.
|
||||
keepAlive := idleTopicTimeout
|
||||
killTimer := time.NewTimer(time.Hour)
|
||||
killTimer.Stop()
|
||||
|
||||
// 'me' only
|
||||
// Notifies about user agent change. 'me' only
|
||||
var uaTimer *time.Timer
|
||||
var currentUA string
|
||||
uaTimer = time.NewTimer(time.Minute)
|
||||
@ -188,7 +188,7 @@ func (t *Topic) run(hub *Hub) {
|
||||
pluginTopic(t, plgActCreate)
|
||||
}
|
||||
} else {
|
||||
if len(t.sessions) == 0 {
|
||||
if len(t.sessions) == 0 && t.cat != types.TopicCatSys {
|
||||
// Failed to subscribe, the topic is still inactive
|
||||
killTimer.Reset(keepAlive)
|
||||
}
|
||||
@ -261,7 +261,7 @@ func (t *Topic) run(hub *Hub) {
|
||||
}
|
||||
|
||||
// If there are no more subscriptions to this topic, start a kill timer
|
||||
if len(t.sessions) == 0 {
|
||||
if len(t.sessions) == 0 && t.cat != types.TopicCatSys {
|
||||
killTimer.Reset(keepAlive)
|
||||
}
|
||||
|
||||
@ -277,12 +277,14 @@ func (t *Topic) run(hub *Hub) {
|
||||
}
|
||||
|
||||
from := types.ParseUserId(msg.Data.From)
|
||||
userData := t.perUser[from]
|
||||
|
||||
if !(userData.modeWant & userData.modeGiven).IsWriter() {
|
||||
msg.sess.queueOut(ErrPermissionDenied(msg.id, t.original(asUid),
|
||||
msg.timestamp))
|
||||
continue
|
||||
userData, userFound := t.perUser[from]
|
||||
// Anyone is allowed to post to 'sys' topic.
|
||||
if t.cat != types.TopicCatSys {
|
||||
if !(userData.modeWant & userData.modeGiven).IsWriter() {
|
||||
msg.sess.queueOut(ErrPermissionDenied(msg.id, t.original(asUid),
|
||||
msg.timestamp))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if err := store.Messages.Save(&types.Message{
|
||||
@ -302,10 +304,11 @@ func (t *Topic) run(hub *Hub) {
|
||||
t.lastID++
|
||||
t.touched = msg.Data.Timestamp
|
||||
msg.Data.SeqId = t.lastID
|
||||
userData.readID = t.lastID
|
||||
userData.readID = t.lastID
|
||||
t.perUser[from] = userData
|
||||
|
||||
if userFound {
|
||||
userData.readID = t.lastID
|
||||
userData.readID = t.lastID
|
||||
t.perUser[from] = userData
|
||||
}
|
||||
if msg.id != "" {
|
||||
reply := NoErrAccepted(msg.id, t.original(asUid), msg.timestamp)
|
||||
reply.Ctrl.Params = map[string]int{"seq": t.lastID}
|
||||
@ -812,7 +815,7 @@ func (t *Topic) subCommonReply(h *Hub, sreg *sessionJoin) error {
|
||||
asLvl := auth.Level(sreg.pkt.authLvl)
|
||||
toriginal := t.original(asUid)
|
||||
|
||||
if !sreg.newsub && (t.cat == types.TopicCatGrp || t.cat == types.TopicCatP2P) {
|
||||
if !sreg.newsub && (t.cat == types.TopicCatP2P || t.cat == types.TopicCatGrp || t.cat == types.TopicCatSys) {
|
||||
// Check if this is a new subscription.
|
||||
_, found := t.perUser[asUid]
|
||||
sreg.newsub = !found
|
||||
@ -913,9 +916,6 @@ func (t *Topic) requestSub(h *Hub, sess *Session, asUid types.Uid, asLvl auth.Le
|
||||
|
||||
if t.cat == types.TopicCatP2P {
|
||||
// P2P could be here only if it was previously deleted. I.e. existingSub is always true for P2P.
|
||||
|
||||
// Undelete
|
||||
userData.deleted = false
|
||||
if modeWant != types.ModeUnset {
|
||||
userData.modeWant = modeWant
|
||||
}
|
||||
@ -923,8 +923,20 @@ func (t *Topic) requestSub(h *Hub, sess *Session, asUid types.Uid, asLvl auth.Le
|
||||
|
||||
// Make sure the user is not asking for unreasonable permissions
|
||||
userData.modeWant = (userData.modeWant & types.ModeCP2P) | types.ModeApprove
|
||||
} else if t.cat == types.TopicCatSys {
|
||||
if asLvl != auth.LevelRoot {
|
||||
sess.queueOut(ErrPermissionDenied(pktID, toriginal, now))
|
||||
return changed, errors.New("subscription to 'sys' topic requires root access level")
|
||||
}
|
||||
|
||||
// Assign default access levels
|
||||
userData.modeWant = types.ModeCSys
|
||||
userData.modeGiven = types.ModeCSys
|
||||
if modeWant != types.ModeUnset {
|
||||
userData.modeWant = (modeWant & types.ModeCSys) | types.ModeWrite
|
||||
}
|
||||
} else {
|
||||
// For non-p2p2 topics access is given as default access
|
||||
// For non-p2p & non-sys topics access is given as default access
|
||||
userData.modeGiven = t.accessFor(asLvl)
|
||||
|
||||
if modeWant == types.ModeUnset {
|
||||
@ -935,6 +947,9 @@ func (t *Topic) requestSub(h *Hub, sess *Session, asUid types.Uid, asLvl auth.Le
|
||||
}
|
||||
}
|
||||
|
||||
// Undelete
|
||||
userData.deleted = false
|
||||
|
||||
if isNullValue(private) {
|
||||
private = nil
|
||||
}
|
||||
@ -1000,9 +1015,6 @@ func (t *Topic) requestSub(h *Hub, sess *Session, asUid types.Uid, asLvl auth.Le
|
||||
// Ownership transfer can only be initiated by the owner
|
||||
sess.queueOut(ErrPermissionDenied(pktID, toriginal, now))
|
||||
return changed, errors.New("non-owner cannot request ownership transfer")
|
||||
} else if t.cat == types.TopicCatP2P {
|
||||
// For P2P topics ignore requests for 'D'. Otherwise it will generate a useless announcement
|
||||
modeWant = (modeWant & types.ModeCP2P) | types.ModeApprove
|
||||
} else if userData.modeGiven.IsAdmin() && modeWant.IsAdmin() {
|
||||
// The Admin should be able to grant any permissions except ownership (checked previously) &
|
||||
// hard-deleting messages.
|
||||
@ -1010,6 +1022,14 @@ func (t *Topic) requestSub(h *Hub, sess *Session, asUid types.Uid, asLvl auth.Le
|
||||
userData.modeGiven |= (modeWant & ^types.ModeDelete)
|
||||
}
|
||||
}
|
||||
|
||||
if t.cat == types.TopicCatP2P {
|
||||
// For P2P topics ignore requests for 'D'. Otherwise it will generate a useless announcement
|
||||
modeWant = (modeWant & types.ModeCP2P) | types.ModeApprove
|
||||
} else if t.cat == types.TopicCatSys {
|
||||
//
|
||||
modeWant &= (modeWant & types.ModeCSys) | types.ModeWrite
|
||||
}
|
||||
}
|
||||
|
||||
// If user has not requested a new access mode, provide one by default.
|
||||
@ -1050,7 +1070,6 @@ func (t *Topic) requestSub(h *Hub, sess *Session, asUid types.Uid, asLvl auth.Le
|
||||
|
||||
// No transactions in RethinkDB, but two owners are better than none
|
||||
if ownerChange {
|
||||
|
||||
oldOwnerData := t.perUser[t.owner]
|
||||
oldOwnerData.modeGiven = (oldOwnerData.modeGiven & ^types.ModeOwner)
|
||||
oldOwnerData.modeWant = (oldOwnerData.modeWant & ^types.ModeOwner)
|
||||
|
154
server/user.go
154
server/user.go
@ -528,18 +528,23 @@ func replyDelUser(s *Session, msg *ClientComMessage) {
|
||||
}
|
||||
}
|
||||
|
||||
type userUpdate struct {
|
||||
// Single user ID being updated
|
||||
uid types.Uid
|
||||
// User IDs being updated
|
||||
uidList []types.Uid
|
||||
// Unread count
|
||||
unread int
|
||||
// Treat the count as an increment as opposite to the final value.
|
||||
inc bool
|
||||
// UserCacheReq contains data which mutates one or more user cache entries.
|
||||
type UserCacheReq struct {
|
||||
// Name of the node sending this request in case of cluster. Not set otherwise.
|
||||
Node string
|
||||
|
||||
// UserId is set when count of unread messages is updated for a single user.
|
||||
UserId types.Uid
|
||||
// UserIdList is set when subscription count is updated for users of a topic.
|
||||
UserIdList []types.Uid
|
||||
// Unread count (UserId is set)
|
||||
Unread int
|
||||
// In case of set UserId: treat Unread count as an increment as opposite to the final value.
|
||||
// In case of set UserIdList: intement (Inc == true) or decrement subscription count by one.
|
||||
Inc bool
|
||||
|
||||
// Optional push notification
|
||||
pushRcpt *push.Receipt
|
||||
PushRcpt *push.Receipt
|
||||
}
|
||||
|
||||
type userCacheEntry struct {
|
||||
@ -553,7 +558,7 @@ var usersCache map[types.Uid]userCacheEntry
|
||||
func usersInit() {
|
||||
usersCache = make(map[types.Uid]userCacheEntry)
|
||||
|
||||
globals.usersUpdate = make(chan *userUpdate, 1024)
|
||||
globals.usersUpdate = make(chan *UserCacheReq, 1024)
|
||||
|
||||
go userUpdater()
|
||||
}
|
||||
@ -566,9 +571,17 @@ func usersShutdown() {
|
||||
}
|
||||
|
||||
func usersUpdateUnread(uid types.Uid, val int, inc bool) {
|
||||
if globals.usersUpdate != nil && (!inc || val != 0) {
|
||||
if globals.usersUpdate == nil || (val == 0 && inc) {
|
||||
return
|
||||
}
|
||||
|
||||
upd := &UserCacheReq{UserId: uid, Unread: val, Inc: inc}
|
||||
if globals.cluster.isRemoteTopic(uid.UserId()) {
|
||||
// Send request to remote node which owns the user.
|
||||
globals.cluster.routeUserReq(upd)
|
||||
} else {
|
||||
select {
|
||||
case globals.usersUpdate <- &userUpdate{uid: uid, unread: val, inc: inc}:
|
||||
case globals.usersUpdate <- upd:
|
||||
default:
|
||||
}
|
||||
}
|
||||
@ -576,9 +589,42 @@ func usersUpdateUnread(uid types.Uid, val int, inc bool) {
|
||||
|
||||
// Process push notification.
|
||||
func usersPush(rcpt *push.Receipt) {
|
||||
if globals.usersUpdate != nil {
|
||||
if globals.usersUpdate == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var local *UserCacheReq
|
||||
|
||||
// In case of a cluster pushes will be initiated at the nodes which own the users.
|
||||
// Sort users into local and remote.
|
||||
if globals.cluster != nil {
|
||||
local = &UserCacheReq{PushRcpt: &push.Receipt{
|
||||
Payload: rcpt.Payload,
|
||||
To: make(map[types.Uid]push.Recipient),
|
||||
}}
|
||||
remote := &UserCacheReq{PushRcpt: &push.Receipt{
|
||||
Payload: rcpt.Payload,
|
||||
To: make(map[types.Uid]push.Recipient),
|
||||
}}
|
||||
|
||||
for uid, recepient := range rcpt.To {
|
||||
if globals.cluster.isRemoteTopic(uid.UserId()) {
|
||||
remote.PushRcpt.To[uid] = recepient
|
||||
} else {
|
||||
local.PushRcpt.To[uid] = recepient
|
||||
}
|
||||
}
|
||||
|
||||
if len(remote.PushRcpt.To) > 0 {
|
||||
globals.cluster.routeUserReq(remote)
|
||||
}
|
||||
} else {
|
||||
local = &UserCacheReq{PushRcpt: rcpt}
|
||||
}
|
||||
|
||||
if len(local.PushRcpt.To) > 0 {
|
||||
select {
|
||||
case globals.usersUpdate <- &userUpdate{pushRcpt: rcpt}:
|
||||
case globals.usersUpdate <- local:
|
||||
default:
|
||||
}
|
||||
}
|
||||
@ -590,17 +636,23 @@ func usersRegisterUser(uid types.Uid, add bool) {
|
||||
return
|
||||
}
|
||||
|
||||
upd := &userUpdate{uidList: make([]types.Uid, 1), inc: add}
|
||||
upd.uidList[0] = uid
|
||||
upd := &UserCacheReq{UserIdList: make([]types.Uid, 1), Inc: add}
|
||||
upd.UserIdList[0] = uid
|
||||
|
||||
select {
|
||||
case globals.usersUpdate <- upd:
|
||||
default:
|
||||
if globals.cluster.isRemoteTopic(uid.UserId()) {
|
||||
// Send request to remote node which owns the user.
|
||||
globals.cluster.routeUserReq(upd)
|
||||
} else {
|
||||
select {
|
||||
case globals.usersUpdate <- upd:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Account users as members of an active topic. Used for cache management.
|
||||
// In case of a cluster this method is called only when the topic is local:
|
||||
// globals.cluster.isRemoteTopic(t.name) == false
|
||||
func usersRegisterTopic(t *Topic, add bool) {
|
||||
if globals.usersUpdate == nil {
|
||||
return
|
||||
@ -611,15 +663,40 @@ func usersRegisterTopic(t *Topic, add bool) {
|
||||
return
|
||||
}
|
||||
|
||||
upd := &userUpdate{uidList: make([]types.Uid, len(t.perUser)), inc: add}
|
||||
i := 0
|
||||
local := &UserCacheReq{Inc: add}
|
||||
|
||||
// In case of a cluster UIDs could be local and remote. Process local UIDs locally,
|
||||
// send remote UIDs to other cluster nodes for processing. The UIDs may have to be
|
||||
// sent to multiple nodes.
|
||||
remote := &UserCacheReq{Inc: add}
|
||||
for uid := range t.perUser {
|
||||
upd.uidList[i] = uid
|
||||
i++
|
||||
if globals.cluster.isRemoteTopic(uid.UserId()) {
|
||||
remote.UserIdList = append(remote.UserIdList, uid)
|
||||
} else {
|
||||
local.UserIdList = append(local.UserIdList, uid)
|
||||
}
|
||||
}
|
||||
|
||||
if len(remote.UserIdList) > 0 {
|
||||
globals.cluster.routeUserReq(remote)
|
||||
}
|
||||
|
||||
if len(local.UserIdList) > 0 {
|
||||
select {
|
||||
case globals.usersUpdate <- local:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// usersRequestFromCluster handles requests which came from other cluser nodes.
|
||||
func usersRequestFromCluster(req *UserCacheReq) {
|
||||
if globals.usersUpdate == nil {
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case globals.usersUpdate <- upd:
|
||||
case globals.usersUpdate <- req:
|
||||
default:
|
||||
}
|
||||
}
|
||||
@ -652,6 +729,12 @@ func userUpdater() {
|
||||
}
|
||||
|
||||
for upd := range globals.usersUpdate {
|
||||
if globals.shuttingDown {
|
||||
// If shutdown is in progress we don't care to process anything.
|
||||
// ignore all calls.
|
||||
continue
|
||||
}
|
||||
|
||||
// Shutdown requested.
|
||||
if upd == nil {
|
||||
globals.usersUpdate = nil
|
||||
@ -660,24 +743,25 @@ func userUpdater() {
|
||||
}
|
||||
|
||||
// Request to send push notifications.
|
||||
if upd.pushRcpt != nil {
|
||||
for uid, rcptTo := range upd.pushRcpt.To {
|
||||
if upd.PushRcpt != nil {
|
||||
for uid, rcptTo := range upd.PushRcpt.To {
|
||||
// Handle update
|
||||
unread := unreadUpdater(uid, 1, true)
|
||||
if unread >= 0 {
|
||||
rcptTo.Unread = unread
|
||||
upd.pushRcpt.To[uid] = rcptTo
|
||||
upd.PushRcpt.To[uid] = rcptTo
|
||||
}
|
||||
}
|
||||
push.Push(upd.pushRcpt)
|
||||
|
||||
push.Push(upd.PushRcpt)
|
||||
continue
|
||||
}
|
||||
|
||||
// Request to add/remove user from cache.
|
||||
if len(upd.uidList) > 0 {
|
||||
for _, uid := range upd.uidList {
|
||||
if len(upd.UserIdList) > 0 {
|
||||
for _, uid := range upd.UserIdList {
|
||||
uce, ok := usersCache[uid]
|
||||
if upd.inc {
|
||||
if upd.Inc {
|
||||
if !ok {
|
||||
// This is a registration of a new user.
|
||||
// We are not loading unread count here, so set it to -1.
|
||||
@ -695,7 +779,7 @@ func userUpdater() {
|
||||
}
|
||||
} else {
|
||||
// BUG!
|
||||
log.Println("ERROR: request to unregister user which has not been registered")
|
||||
log.Println("ERROR: request to unregister user which has not been registered", uid)
|
||||
}
|
||||
}
|
||||
|
||||
@ -703,7 +787,7 @@ func userUpdater() {
|
||||
}
|
||||
|
||||
// Request to update unread count.
|
||||
unreadUpdater(upd.uid, upd.unread, upd.inc)
|
||||
unreadUpdater(upd.UserId, upd.Unread, upd.Inc)
|
||||
}
|
||||
|
||||
log.Println("users: shutdown")
|
||||
|
@ -31,15 +31,18 @@ type validator struct {
|
||||
ValidationSubject string `json:"validation_subject"`
|
||||
ResetSubject string `json:"reset_subject"`
|
||||
SendFrom string `json:"sender"`
|
||||
Login string `json:"login"`
|
||||
SenderPassword string `json:"sender_password"`
|
||||
DebugResponse string `json:"debug_response"`
|
||||
MaxRetries int `json:"max_retries"`
|
||||
SMTPAddr string `json:"smtp_server"`
|
||||
SMTPPort string `json:"smtp_port"`
|
||||
Domains []string `json:"domains"`
|
||||
|
||||
htmlValidationTempl *ht.Template
|
||||
htmlResetTempl *ht.Template
|
||||
auth smtp.Auth
|
||||
senderEmail string
|
||||
}
|
||||
|
||||
const (
|
||||
@ -63,12 +66,19 @@ func (v *validator) Init(jsonconf string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// SendFrom could be an RFC 5322 address of the form "John Doe <jdoe@example.com>". Parse it.
|
||||
if sender, err := mail.ParseAddress(v.SendFrom); err == nil {
|
||||
v.auth = smtp.PlainAuth("", sender.Address, v.SenderPassword, v.SMTPAddr)
|
||||
} else {
|
||||
sender, err := mail.ParseAddress(v.SendFrom)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v.senderEmail = sender.Address
|
||||
|
||||
// Check if login is provided explicitly. Otherwise parse Sender and use that as login for authentication.
|
||||
if v.Login != "" {
|
||||
v.auth = smtp.PlainAuth("", v.Login, v.SenderPassword, v.SMTPAddr)
|
||||
} else {
|
||||
// SendFrom could be an RFC 5322 address of the form "John Doe <jdoe@example.com>". Parse it.
|
||||
v.auth = smtp.PlainAuth("", v.senderEmail, v.SenderPassword, v.SMTPAddr)
|
||||
}
|
||||
|
||||
// If a relative path is provided, try to resolve it relative to the exec file location,
|
||||
// not whatever directory the user is in.
|
||||
@ -158,8 +168,6 @@ func (v *validator) PreCheck(cred string, params interface{}) error {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Check email uniqueness
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -270,7 +278,7 @@ func (v *validator) Remove(user t.Uid, value string) error {
|
||||
// Here are instructions for Google cloud:
|
||||
// https://cloud.google.com/appengine/docs/standard/go/mail/sending-receiving-with-mail-api
|
||||
func (v *validator) send(to, subj, body string) error {
|
||||
err := smtp.SendMail(v.SMTPAddr+":"+v.SMTPPort, v.auth, v.SendFrom, []string{to},
|
||||
err := smtp.SendMail(v.SMTPAddr+":"+v.SMTPPort, v.auth, v.senderEmail, []string{to},
|
||||
[]byte("From: "+v.SendFrom+
|
||||
"\nTo: "+to+
|
||||
"\nSubject: "+subj+
|
||||
|
@ -194,7 +194,7 @@ func main() {
|
||||
log.Fatal("Failed to parse config file:", err)
|
||||
}
|
||||
|
||||
err := store.Open(1, string(config.StoreConfig))
|
||||
err := store.Open(1, config.StoreConfig)
|
||||
defer store.Close()
|
||||
|
||||
if err != nil {
|
||||
@ -222,13 +222,13 @@ func main() {
|
||||
|
||||
if *upgrade {
|
||||
// Upgrade DB from one version to another.
|
||||
err = store.UpgradeDb(string(config.StoreConfig))
|
||||
err = store.UpgradeDb(config.StoreConfig)
|
||||
if err == nil {
|
||||
log.Println("Database successfully upgraded")
|
||||
}
|
||||
} else {
|
||||
// Reset or create DB
|
||||
err = store.InitDb(string(config.StoreConfig), true)
|
||||
err = store.InitDb(config.StoreConfig, true)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
Reference in New Issue
Block a user