Browse Source

create BaseForm to enable CSRF

register page

redirect user to dashboard if they are logged in

enable csrf for login page

Set models more strict

bootstrap developer page

add helper method to ModelMixin, remove CRUDMixin

display list of clients on developer index, add copy client-secret to clipboard using clipboardjs

add toastr and use jquery non slim

display a toast when user copies the client-secret

create new client, generate client-id using unidecode

client detail page: can edit client

add delete client

implement /oauth/authorize and /oauth/allow-deny

implement /oauth/token

add /oauth/user_info endpoint

handle scopes: wip

take into account scope: display scope, return user data according to scope

create virtual-domain, gen email, client_user model WIP

create authorize_nonlogin_user page

user can choose to generate a new email

no need to interfere with root logger

log for before and after request

if user has already allowed a client: generate a auth-code and redirect user to client

get_user_info takes into account gen email

display list of clients that have user has authorised

use yk-client domain instead of localhost as cookie depends on the domain name

use wtforms instead of flask_wtf

Dockerfile

delete virtual domain

EMAIL_DOMAIN can come from env var

bind to host 0.0.0.0

fix signup error: use session as default csrf_context

rename yourkey to simplelogin

add python-dotenv, ipython, sqlalchemy_utils

create DB_URI, FLASK_SECRET. Load config from CONFIG file if exist

add shortcuts to logging

create shell

add psycopg2

do not add local data in Dockerfile

add drop_db into shell

add shell.prepare_db()

fix prepare_db

setup sentry

copy assets from tabler/dist

add icon downloaded from https://commons.wikimedia.org/wiki/File:Simpleicons_Interface_key-tool-1.svg

integrate tabler - login and register page

add favicon

template: default, header. Use gravatar for user avatar url

use default template for dashboard, developer page

use another icon

add clipboard and notie

prettify dashboard

add notie css

add fake gen email and client-user

prettify list client page, use notie for toast

add email, name scope to new client

display client scope in client list

prettify new-client, client-detail

add sentry-sdk and blinker

add arrow, add dt jinja filter, prettify logout, dashboard

comment "last used" in dashboard for now

prettify date display

add copy email to clipboard to dashboard

use "users" as table name for User as "user" is reserved key in postgres

call prepare_db() when creating new db

error page 400, 401, 403, 404

prettify authorize_login_user

create already_authorize.html for user who has already authorized a client

user can generate new email

display all other generated emails

add ENV variable, only reset DB when ENV=local

fix: not return other users gen emails

display nb users for each client

refactor shell: remove prepare_db()

add sendgrid

add /favicon.ico route

add new config: URL, SUPPORT_EMAIL, SENDGRID_API_KEY

user needs to activate their account before login

create copy button on dashboard

client can have multiple redirect uris, in client detail can add/remove redirect-uri,

use redirect_uri passed in /authorize

refactor: move get_user_info into ClientUser model

dashboard: display all apps, all generated emails

add "id" into user_info

add trigger email button

invalidate the session at each new version by changing the secret

centralize Client creation into Client.create_new

user can enable/disable email forwarding

setup auto dismiss alert: just add .alert-auto-dismiss

move name down in register form

add shell.add_real_data

move blueprint template to its own package

prettify authorize page for non-authenticated user

update readme, return error if not redirect_uri

add flask-wtf, use psycopg2-binary

use flask-wtf FlaskForm instead of Form

rename email -> email_utils

add AWS_REGION, BUCKET, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY to config

add s3 module

add File model, add Client.icon_id

handle client icon update

can create client with icon

display client icon in client list page

add Client.home_url

take into account Client.home_url

add boto3

register: ask name first

only show "trigger test email" if email forwarding is enabled

display gen email in alphabetical order, client in client.name alphabetical order

better error page

the modal does not get close when user clicks outside of modal

add Client.published column

discover page that displays all published Client

add missing bootstrap.bundle.min.js.map

developer can publish/unpublish their app in discover

use notie for display flash message

create hotmail account

fix missing jquery

add footer, add global jinja2 variable

strengthen model: use nullable=False whenever possible,

rename client_id to oauth_client_id, client_secret to oauth_client_secret

add flask-migrate

init migrate

1st migrate version

fix rename client_id -> oauth_client_id

prettify UI

use flask_migrate.upgrade() instead of db.create_all()

make sure requirejs.config is called for all page

enable sentry for js, use uppercase for global jinja2 variables

add flask-admin

add User.is_admin column

setup flask admin, only accessible to admin user

fix migration: add server_default

replace session[redirect_after_login] by "next" request args

add pyproject.toml: ignore migrations/ in black

add register waiting_activation_email page

better email wording

add pytest

add get_host_name_and_scheme and tests

example fail test

fix test

fix client-id display

add flask-cors

/user_info supports cors, add /me as /user_info synonym

return client in /me

support implicit flow

no need to use with "app.app_context()"

add watchtower to requirement

add param ENABLE_CLOUDWATCH, CLOUDWATCH_LOG_GROUP, CLOUDWATCH_LOG_STREAM

add cloudwatch logger if cloudwatch is enabled

add 500 error page

add help text for list of used client

display list of app/website that an email has been used

click on client name brings to client detail page

create style.css to add additional style, append its url with the current sha1 to avoid cache

POC on how to send email using postfix

add sqlalchemy-utils

use arrow instead of datetime

add new params STRIPE_API, STRIPE_YEARLY_SKU, STRIPE_MONTHLY_PLAN

show full error in local

add plan, plan_expiration to User, need to create enum directly in migration script, cf https://github.com/sqlalchemy/alembic/issues/67

reformat all html files: use space instead of tab

new user will have trial plan for 15 days

add new param MAX_NB_EMAIL_FREE_PLAN

only user with enough quota can create new email

if user cannot create new gen email, pick randomly one from existing gen emails. Use flush instead of commit

rename STRIPE_YEARLY_SKU -> STRIPE_YEARLY_PLAN

open client page in discover in a new tab

add stripe

not logging /static call: disable flask logging, replace by after_request

add param STRIPE_SECRET_KEY

add 3 columns stripe_customer_id, stripe_card_token, stripe_subscription_id

user can upgrade their pricing

add setting page as coming-soon

add GenEmail, ClientUser to admin

ignore /admin/static logging

add more fake data

add ondelete="cascade" whenever possible

rename plan_expiration -> trial_expiration

reset migration: delete old migrations, create new one

rename test_send_email -> poc_send_email to avoid the file being called by pytest

add new param LYRA_ANALYTICS_ID, add lyra analytics

add how to create new migration into readme

add drift to base.html

notify admin when new user signs up or pays subscription

log exception in case of 500

use sendgrid to notify admin

add alias /userinfo to user_info endpoint

add change_password to shell

add info on how payment is handled

invite user to retry if card not working

remove drift and add "contact us" link

move poc_send_email into poc/

support getting client-id, client-secret from form-data in addition to basic auth

client-id, client-secret is passed in form-data by passport-oauth2 for ex

add jwtRS256 private and public key

add jwk-jws-jwt poc

add new param OPENID_PRIVATE_KEY_PATH, OPENID_PRIVATE_KEY_PATH

add scope, redirect_url to AuthorizationCode and OauthToken

take into scope when creating oauth-token, authorization-code

add jwcrypto

add jose_utils: make_id_token and verify_id_token

add &scope to redirect uri

add "email_verified": True into user_info

fix user not activated

add /oauth2 as alias for /oauth

handle case where scope and state are empty

remove threaded=False

Use Email Alias as wording

remove help text

user can re-send activation email

add "expired" into ActivationCode

Handle the case activation code is expired

reformat: use form.validate_on_submit instead of request.method == post && form.validate

use error text instead of flash()

display client oauth-id and oauth-secret on client detail page

not display oauth-secret on client listing

fix expiration check

improve page title, footer

add /jwks and /.well-known/openid-configuration

init properly tests, fix blueprint conflict bug in flask-admin

create oauth_models module

rename Scope -> ScopeE to distinguish with Scope DB model

set app.url_map.strict_slashes = False

use ScopeE instead of SCOPE_NAME, ...

support access_token passed as args in /userinfo

merge /allow-deny into /authorize

improve wording

take into account the case response_type=code and openid is in scope

take into account response_type=id_token, id_token token, id_token code

make sure to use in-memory db in test

fix scope can be null

allow cross_origin for /.well-known/openid-configuration and /jwks

fix footer link

center authorize form

rename trial_expiration to plan_expiration

move stripe init to create_app()

use real email to be able to receive email notification

add user.profile_picture_id column

use user profile picture and fallback to gravatar

use nguyenkims+local@gm to distinguish with staging

handle plan cancel, reactivation, user profile update

fix can_create_new_email

create cron.py that set plan to free when expired

add crontab.yml

add yacron

use notify_admin instead of LOG.error

add ResetPasswordCode model

user can change password in setting

increase display time for notie

add forgot_password page

If login error: redirect to this page upon success login.

hide discover tab

add column user.is_developer

only show developer menu to developer

comment out the publish button

set local user to developer

make sure only developer can access /developer blueprint

User is invited to upgrade if they are in free plan or their trial ends soon

not sending email when in local mode

create Partner model

create become partner page

use normal error handling on local

fix migration

add "import sqlalchemy_utils" into migration template

small refactoring on setting page

handle promo code. TODO: add migration file

add migration for user.promo_codes

move email alias on top of apps in dashboard

add introjs

move encode_url to utils

create GenEmail.create_new_gen_email

create a first alias mail to show user how to use when they login

show intro when user visits the website the first time

fix register
Son NK 6 years ago
parent
commit
c18d9f5280
100 changed files with 39163 additions and 112 deletions
  1. 1 0
      .dockerignore
  2. 4 1
      .gitignore
  3. 15 0
      Dockerfile
  4. 110 0
      README.md
  5. 22 0
      app/admin_model.py
  6. 9 1
      app/auth/__init__.py
  7. 3 1
      app/auth/base.py
  8. 16 0
      app/auth/templates/auth/activate.html
  9. 37 0
      app/auth/templates/auth/forgot_password.html
  10. 63 0
      app/auth/templates/auth/login.html
  11. 14 0
      app/auth/templates/auth/logout.html
  12. 52 0
      app/auth/templates/auth/register.html
  13. 22 0
      app/auth/templates/auth/register_waiting_activation.html
  14. 31 0
      app/auth/templates/auth/resend_activation.html
  15. 31 0
      app/auth/templates/auth/reset_password.html
  16. 56 0
      app/auth/views/activate.py
  17. 30 0
      app/auth/views/forgot_password.py
  18. 33 18
      app/auth/views/login.py
  19. 89 0
      app/auth/views/register.py
  20. 39 0
      app/auth/views/resend_activation.py
  21. 59 0
      app/auth/views/reset_password.py
  22. 59 0
      app/config.py
  23. 1 1
      app/dashboard/__init__.py
  24. 4 1
      app/dashboard/base.py
  25. 244 0
      app/dashboard/templates/dashboard/index.html
  26. 182 0
      app/dashboard/templates/dashboard/pricing.html
  27. 101 0
      app/dashboard/templates/dashboard/setting.html
  28. 83 4
      app/dashboard/views/index.py
  29. 90 0
      app/dashboard/views/pricing.py
  30. 173 0
      app/dashboard/views/setting.py
  31. 1 0
      app/developer/__init__.py
  32. 18 0
      app/developer/base.py
  33. 150 0
      app/developer/templates/developer/client_detail.html
  34. 164 0
      app/developer/templates/developer/index.html
  35. 37 0
      app/developer/templates/developer/new_client.html
  36. 0 0
      app/developer/views/__init__.py
  37. 71 0
      app/developer/views/client_detail.py
  38. 52 0
      app/developer/views/index.py
  39. 50 0
      app/developer/views/new_client.py
  40. 1 0
      app/discover/__init__.py
  41. 8 0
      app/discover/base.py
  42. 41 0
      app/discover/templates/discover/index.html
  43. 0 0
      app/discover/views/__init__.py
  44. 12 0
      app/discover/views/index.py
  45. 38 0
      app/email_utils.py
  46. 4 30
      app/extensions.py
  47. 47 0
      app/jose_utils.py
  48. 50 14
      app/log.py
  49. 370 16
      app/models.py
  50. 1 4
      app/monitor/views.py
  51. 1 0
      app/oauth/__init__.py
  52. 5 0
      app/oauth/base.py
  53. 81 0
      app/oauth/templates/oauth/authorize.html
  54. 39 0
      app/oauth/templates/oauth/authorize_nonlogin_user.html
  55. 0 0
      app/oauth/views/__init__.py
  56. 197 0
      app/oauth/views/authorize.py
  57. 88 0
      app/oauth/views/token.py
  58. 30 0
      app/oauth/views/user_info.py
  59. 57 0
      app/oauth_models.py
  60. 1 0
      app/partner/__init__.py
  61. 8 0
      app/partner/base.py
  62. 58 0
      app/partner/templates/partner/become.html
  63. 0 0
      app/partner/views/__init__.py
  64. 77 0
      app/partner/views/become.py
  65. 38 0
      app/s3.py
  66. 15 0
      app/utils.py
  67. 37 0
      cron.py
  68. 6 0
      crontab.yml
  69. 51 0
      local_data/jwtRS256.key
  70. 14 0
      local_data/jwtRS256.key.pub
  71. 45 0
      migrations/alembic.ini
  72. 96 0
      migrations/env.py
  73. 25 0
      migrations/script.py.mako
  74. 29 0
      migrations/versions/0256244cd7c8_.py
  75. 24 0
      migrations/versions/213fcca48483_.py
  76. 28 0
      migrations/versions/2fe19381f386_.py
  77. 34 0
      migrations/versions/3cd10cfce8c3_.py
  78. 29 0
      migrations/versions/590d89f981c0_.py
  79. 172 0
      migrations/versions/5e549314e1e2_.py
  80. 40 0
      migrations/versions/b20ee72fd9a4_.py
  81. 39 0
      migrations/versions/d03e433dc248_.py
  82. 30 0
      migrations/versions/f234688f5ebd_.py
  83. 0 0
      poc/__init__.py
  84. 17 0
      poc/jwt-jws-jwk.py
  85. 33 0
      poc/poc_send_email.py
  86. 18 0
      pyproject.toml
  87. 27 0
      requirements.in
  88. 89 4
      requirements.txt
  89. 202 17
      server.py
  90. 65 0
      shell.py
  91. 16745 0
      static/assets/css/dashboard.css
  92. 16745 0
      static/assets/css/dashboard.rtl.css
  93. BIN
      static/assets/fonts/feather/feather-webfont.eot
  94. 1038 0
      static/assets/fonts/feather/feather-webfont.svg
  95. BIN
      static/assets/fonts/feather/feather-webfont.ttf
  96. BIN
      static/assets/fonts/feather/feather-webfont.woff
  97. 0 0
      static/assets/images/browsers/android-browser.svg
  98. 1 0
      static/assets/images/browsers/aol-explorer.svg
  99. 1 0
      static/assets/images/browsers/blackberry.svg
  100. 0 0
      static/assets/images/browsers/camino.svg

+ 1 - 0
.dockerignore

@@ -0,0 +1 @@
+db.sqlite

+ 4 - 1
.gitignore

@@ -1,3 +1,6 @@
 .idea/
 *.pyc
-db.sqlite
+db.sqlite
+.env
+.pytest_cache
+.vscode

+ 15 - 0
Dockerfile

@@ -0,0 +1,15 @@
+FROM python:3.7
+
+RUN apt-get update
+
+WORKDIR /code
+
+COPY ./requirements.txt ./
+RUN pip3 install -r requirements.txt
+
+
+COPY . .
+
+CMD gunicorn wsgi:app -b 0.0.0.0:5000 -w 2 --timeout 15 --log-level DEBUG
+
+#CMD ["/usr/local/bin/gunicorn", "wsgi:app", "-k", "gthread", "-b", "0.0.0.0:5000", "-w", "2", "--timeout", "15", "--log-level", "DEBUG"]

+ 110 - 0
README.md

@@ -0,0 +1,110 @@
+
+## OAuth flow
+
+Authorization code flow: 
+
+http://sl-server:5000/oauth/authorize?client_id=client-id&state=123456&response_type=code&redirect_uri=http%3A%2F%2Fsl-client%3A7000%2Fcallback&state=dvoQ6Jtv0PV68tBUgUMM035oFiZw57
+
+Implicit flow:
+http://sl-server:5000/oauth/authorize?client_id=client-id&state=123456&response_type=token&redirect_uri=http%3A%2F%2Fsl-client%3A7000%2Fcallback&state=dvoQ6Jtv0PV68tBUgUMM035oFiZw57
+
+Exchange the code to get the token with `{code}` replaced by the code obtained in previous step.
+
+http -f -a client-id:client-secret http://localhost:5000/oauth/token grant_type=authorization_code code={code}
+
+Get user info:
+
+http http://localhost:5000/oauth/user_info 'Authorization:Bearer {token}'
+
+
+## Template structure
+
+base
+    single: for login, register page
+    default: for all pages when user log ins
+        
+## How to create new migration
+
+Whenever the model changes, a new migration needs to be created
+
+Set the database connection to use staging environment:
+
+> set -x CONFIG ~/config/simplelogin/staging.env
+
+Generate the migration script and make sure to review it:
+
+> flask db migrate
+
+## Code structure
+
+local_data/: contain files used only locally. In deployment, these files should be replaced.
+    - jwtRS256.key: generated using 
+    
+```bash
+ssh-keygen -t rsa -b 4096 -m PEM -f jwtRS256.key
+# Don't add passphrase
+openssl rsa -in jwtRS256.key -pubout -outform PEM -out jwtRS256.key.pub
+```
+
+## OpenID, OAuth2 response_type & scope
+
+According to https://medium.com/@darutk/diagrams-of-all-the-openid-connect-flows-6968e3990660
+
+- `response_type` can be either `code, token, id_token`  or any combination.
+- `scope` can contain `openid` or not
+
+Below is the different combinations that are taken into account until now:
+
+response_type=code
+    scope:
+	    with `openid` in scope, return `id_token` at /token: OK
+	    without: OK
+
+response_type=token
+    scope:
+	    with and without `openid`, nothing to do: OK
+
+response_type=id_token
+    return `id_token` in /authorization endpoint
+    
+response_type=id_token token
+    return `id_token` in addition to `access_token` in /authorization endpoint
+   
+response_type=id_token code
+    return `id_token` in addition to `authorization_code` in /authorization endpoint
+   
+
+# Plan Upgrade, downgrade flow
+
+Here's an example:
+
+July 2019: user takes yearly plan, valid until July 2020
+    user.plan=yearly, user.plan_expiration=None
+    set user.stripe card-token, customer-id, subscription-id
+
+December 2019: user cancels his plan.
+	set plan_expiration to "period end of subscription", ie July 2020
+	call stripe:
+		stripe.Subscription.modify(
+		  user.stripe_subscription_id,
+		  cancel_at_period_end=True
+		)
+
+There are 2 possible scenarios at this point:
+1) user decides to renew on March 2020: 
+	set plan_expiration = None
+	stripe.Subscription.modify(
+	  user.stripe_subscription_id,
+	  cancel_at_period_end=False
+	)
+
+2) the plan ends on July 2020. 
+The cronjob set 
+- user stripe_subscription_id , stripe_card_token, stripe_customer_id to None
+- user.plan=free, user.plan_expiration=None
+- delete customer on stripe
+
+user decides to take the premium plan again: go through all normal flow
+
+
+

+ 22 - 0
app/admin_model.py

@@ -0,0 +1,22 @@
+from flask import redirect, url_for, request
+from flask_admin import expose, AdminIndexView
+from flask_admin.contrib import sqla
+from flask_login import current_user
+
+
+class SLModelView(sqla.ModelView):
+    def is_accessible(self):
+        return current_user.is_authenticated and current_user.is_admin
+
+    def inaccessible_callback(self, name, **kwargs):
+        # redirect to login page if user doesn't have access
+        return redirect(url_for("auth.login", next=request.url))
+
+
+class SLAdminIndexView(AdminIndexView):
+    @expose("/")
+    def index(self):
+        if not current_user.is_authenticated or not current_user.is_admin:
+            return redirect(url_for("auth.login", next=request.url))
+
+        return super(SLAdminIndexView, self).index()

+ 9 - 1
app/auth/__init__.py

@@ -1 +1,9 @@
-from .views import login, logout
+from .views import (
+    login,
+    logout,
+    register,
+    activate,
+    resend_activation,
+    reset_password,
+    forgot_password,
+)

+ 3 - 1
app/auth/base.py

@@ -1,3 +1,5 @@
 from flask import Blueprint
 
-auth_bp = Blueprint(name="auth", import_name=__name__, url_prefix="/auth")
+auth_bp = Blueprint(
+    name="auth", import_name=__name__, url_prefix="/auth", template_folder="templates"
+)

+ 16 - 0
app/auth/templates/auth/activate.html

@@ -0,0 +1,16 @@
+{% extends "error.html" %}
+
+{% block error_name %}
+  {{ error }}
+{% endblock %}
+
+{% block error_description %}
+
+  {% if show_resend_activation %}
+    <div class="text-center text-muted small mt-4">
+      Ask for another activation email?
+      <a href="{{ url_for('auth.resend_activation') }}" style="color: #4d21ff">Resend</a>
+    </div>
+  {% endif %}
+
+{% endblock %}

+ 37 - 0
app/auth/templates/auth/forgot_password.html

@@ -0,0 +1,37 @@
+{% from "_formhelpers.html" import render_field, render_field_errors %}
+
+{% extends "single.html" %}
+
+{% block title %}
+  Forgot Password
+{% endblock %}
+
+{% block single_content %}
+  {% if error %}
+    <div class="text-danger text-center mb-4">{{ error }}</div>
+  {% endif %}
+
+  <form class="card" method="post">
+    {{ form.csrf_token }}
+    <div class="card-body p-6">
+      <div class="card-title">Forgot password</div>
+      <p class="text-muted">Enter your email address and your will receive an email to reset your password.</p>
+
+      <div class="form-group">
+        <label class="form-label">Email address</label>
+        {{ form.email(class="form-control", type="email", placeholder="Enter email") }}
+        {{ render_field_errors(form.email) }}
+      </div>
+
+      <div class="form-footer">
+        <button type="submit" class="btn btn-primary btn-block">Reset Password</button>
+      </div>
+    </div>
+  </form>
+
+  <div class="text-center text-muted">
+    Forget it, <a href="{{ url_for('auth.login') }}">send me back</a> to the sign in screen.
+  </div>
+
+
+{% endblock %}

+ 63 - 0
app/auth/templates/auth/login.html

@@ -0,0 +1,63 @@
+{% from "_formhelpers.html" import render_field, render_field_errors %}
+
+{% extends "single.html" %}
+
+{% block title %}
+  Login
+{% endblock %}
+
+{% block single_content %}
+  {% if error %}
+    <div class="text-danger text-center mb-4">{{ error }}</div>
+  {% endif %}
+
+  {% if show_resend_activation %}
+    <div class="text-center text-muted small mb-4">
+      You haven't received the activation email?
+      <a href="{{ url_for('auth.resend_activation') }}">Resend</a>
+    </div>
+  {% endif %}
+
+  <form class="card" method="post">
+    {{ form.csrf_token }}
+    <div class="card-body p-6">
+      <div class="card-title">Login to your account</div>
+
+      <div class="form-group">
+        <label class="form-label">Email address</label>
+        {{ form.email(class="form-control", type="email") }}
+        {{ render_field_errors(form.email) }}
+      </div>
+
+      <div class="form-group">
+        <label class="form-label">
+          Password
+          <a href="{{ url_for('auth.forgot_password') }}" class="float-right small">
+            I forgot password
+          </a>
+        </label>
+        {{ form.password(class="form-control", type="password") }}
+        {{ render_field_errors(form.password) }}
+      </div>
+
+      <!-- TODO: add remember me
+      <div class="form-group">
+        <label class="custom-control custom-checkbox">
+          <input type="checkbox" class="custom-control-input"/>
+          <span class="custom-control-label">Remember me</span>
+        </label>
+      </div>
+      -->
+
+      <div class="form-footer">
+        <button type="submit" class="btn btn-primary btn-block">Sign in</button>
+      </div>
+    </div>
+  </form>
+
+  <div class="text-center text-muted">
+    Don't have account yet? <a href="{{ url_for('auth.register') }}">Sign up</a>
+  </div>
+
+
+{% endblock %}

+ 14 - 0
app/auth/templates/auth/logout.html

@@ -0,0 +1,14 @@
+{% extends "single.html" %}
+
+{% block title %}
+  Logout
+{% endblock %}
+
+{% block single_content %}
+  <div class="text-center text-muted">
+    You are logged out.
+
+    <a href="{{ url_for('auth.login') }}">Login</a>
+  </div>
+
+{% endblock %}

+ 52 - 0
app/auth/templates/auth/register.html

@@ -0,0 +1,52 @@
+{% from "_formhelpers.html" import render_field, render_field_errors %}
+
+{% extends "single.html" %}
+
+{% block title %}
+  Register
+{% endblock %}
+
+{% block single_content %}
+  <form class="card" method="post">
+    {{ form.csrf_token }}
+    <div class="card-body p-6">
+      <div class="card-title">Create new account</div>
+
+      <div class="form-group">
+        <label class="form-label">How should we call you?</label>
+        {{ form.name(class="form-control") }}
+        {{ render_field_errors(form.name) }}
+      </div>
+
+      <div class="form-group">
+        <label class="form-label">Email address</label>
+        {{ form.email(class="form-control", type="email") }}
+        {{ render_field_errors(form.email) }}
+      </div>
+
+      <div class="form-group">
+        <label class="form-label">Password</label>
+        {{ form.password(class="form-control", type="password") }}
+        {{ render_field_errors(form.password) }}
+      </div>
+
+      <!-- TODO: add terms
+      <div class="form-group">
+        <label class="custom-control custom-checkbox">
+          <input type="checkbox" class="custom-control-input"/>
+          <span class="custom-control-label">Agree the <a href="terms.html">terms and policy</a></span>
+        </label>
+      </div>
+      -->
+
+      <div class="form-footer">
+        <button type="submit" class="btn btn-primary btn-block">Create new account</button>
+      </div>
+    </div>
+  </form>
+
+  <div class="text-center text-muted">
+    Already have account? <a href="{{ url_for('auth.login') }}">Sign in</a>
+  </div>
+
+{% endblock %}

+ 22 - 0
app/auth/templates/auth/register_waiting_activation.html

@@ -0,0 +1,22 @@
+{% extends "single.html" %}
+
+{% block title %}
+  Activation Email Sent
+{% endblock %}
+
+{% block single_content %}
+  <div class="text-center">
+    <h1>
+      An email to validate your email is on its way.
+    </h1>
+
+    <h3>
+      Please check your inbox/spam folder.
+    </h3>
+    <small>
+      Yeah we know. An email to confirm an email ...
+    </small>
+    </h1>
+  </div>
+
+{% endblock %}

+ 31 - 0
app/auth/templates/auth/resend_activation.html

@@ -0,0 +1,31 @@
+{% from "_formhelpers.html" import render_field, render_field_errors %}
+
+{% extends "single.html" %}
+
+{% block title %}
+  Resend activation email
+{% endblock %}
+
+{% block single_content %}
+  <form class="card" method="post">
+    {{ form.csrf_token }}
+    <div class="card-body p-6">
+      <div class="card-title">Resend activation email</div>
+
+      <div class="form-group">
+        <label class="form-label">Email address</label>
+        {{ form.email(class="form-control", type="email") }}
+        {{ render_field_errors(form.email) }}
+      </div>
+
+      <div class="form-footer">
+        <button type="submit" class="btn btn-primary btn-block">Resend</button>
+      </div>
+    </div>
+  </form>
+
+  <div class="text-center text-muted">
+    Don't have account yet? <a href="{{ url_for('auth.register') }}">Sign up</a>
+  </div>
+
+{% endblock %}

+ 31 - 0
app/auth/templates/auth/reset_password.html

@@ -0,0 +1,31 @@
+{% from "_formhelpers.html" import render_field, render_field_errors %}
+
+{% extends "single.html" %}
+
+{% block title %}
+  Reset password
+{% endblock %}
+
+{% block single_content %}
+  {% if error %}
+    <div class="text-danger text-center mb-4">{{ error }}</div>
+  {% endif %}
+
+  <form class="card" method="post">
+    {{ form.csrf_token }}
+    <div class="card-body p-6">
+      <div class="card-title">Reset your password</div>
+
+      <div class="form-group">
+        <label class="form-label">Password</label>
+        {{ form.password(class="form-control", type="password") }}
+        {{ render_field_errors(form.password) }}
+      </div>
+
+      <div class="form-footer">
+        <button type="submit" class="btn btn-primary btn-block">Reset</button>
+      </div>
+    </div>
+  </form>
+
+{% endblock %}

+ 56 - 0
app/auth/views/activate.py

@@ -0,0 +1,56 @@
+import arrow
+from flask import request, redirect, url_for, flash, render_template
+from flask_login import login_user, current_user
+
+from app.auth.base import auth_bp
+from app.extensions import db
+from app.log import LOG
+from app.models import ActivationCode
+
+
+@auth_bp.route("/activate", methods=["GET", "POST"])
+def activate():
+    if current_user.is_authenticated:
+        return (
+            render_template("auth/activate.html", error="You are already logged in"),
+            400,
+        )
+
+    code = request.args.get("code")
+
+    activation_code: ActivationCode = ActivationCode.get_by(code=code)
+
+    if not activation_code:
+        return (
+            render_template("auth/activate.html", error="Activation code not found"),
+            400,
+        )
+
+    if activation_code.expired and activation_code.expired < arrow.now():
+        return (
+            render_template(
+                "auth/activate.html",
+                error="Activation code is expired",
+                show_resend_activation=True,
+            ),
+            400,
+        )
+
+    user = activation_code.user
+    user.activated = True
+    login_user(user)
+
+    # activation code is to be used only once
+    activation_code.delete()
+    db.session.commit()
+
+    flash("Your account has been activated", "success")
+
+    # The activation link contains the original page, for ex authorize page
+    if "next" in request.args:
+        next_url = request.args.get("next")
+        LOG.debug("redirect user to %s", next_url)
+        return redirect(next_url)
+    else:
+        LOG.debug("redirect user to dashboard")
+        return redirect(url_for("dashboard.index"))

+ 30 - 0
app/auth/views/forgot_password.py

@@ -0,0 +1,30 @@
+from flask import request, render_template, redirect, url_for
+from flask_wtf import FlaskForm
+from wtforms import StringField, validators
+
+from app.auth.base import auth_bp
+from app.dashboard.views.setting import send_reset_password_email
+from app.models import User
+
+
+class ForgotPasswordForm(FlaskForm):
+    email = StringField("Email", validators=[validators.DataRequired()])
+
+
+@auth_bp.route("/forgot_password", methods=["GET", "POST"])
+def forgot_password():
+    form = ForgotPasswordForm(request.form)
+
+    if form.validate_on_submit():
+        email = form.email.data
+
+        user = User.get_by(email=email)
+
+        if not user:
+            error = "No such user, are you sure the email is correct?"
+            return render_template("auth/forgot_password.html", form=form, error=error)
+
+        send_reset_password_email(user)
+        return redirect(url_for("auth.forgot_password"))
+
+    return render_template("auth/forgot_password.html", form=form)

+ 33 - 18
app/auth/views/login.py

@@ -1,13 +1,14 @@
-from flask import request, flash, render_template, redirect, url_for
+from flask import request, render_template, redirect, url_for
 from flask_login import login_user
-from wtforms import Form, StringField, validators
+from flask_wtf import FlaskForm
+from wtforms import StringField, validators
 
 from app.auth.base import auth_bp
 from app.log import LOG
 from app.models import User
 
 
-class LoginForm(Form):
+class LoginForm(FlaskForm):
     email = StringField("Email", validators=[validators.DataRequired()])
     password = StringField("Password", validators=[validators.DataRequired()])
 
@@ -16,21 +17,35 @@ class LoginForm(Form):
 def login():
     form = LoginForm(request.form)
 
-    if request.method == "POST":
-        if form.validate():
-            user = User.query.filter_by(email=form.email.data).first()
-
-            if not user:
-                flash("No such email", "warning")
-                return render_template("auth/login.html", form=form)
-
-            if not user.check_password(form.password.data):
-                flash("Wrong password", "warning")
-                return render_template("auth/login.html", form=form)
-
-            LOG.debug("log user %s in", user)
-            login_user(user)
-
+    if form.validate_on_submit():
+        user = User.filter_by(email=form.email.data).first()
+
+        if not user:
+            return render_template(
+                "auth/login.html", form=form, error="Email not exist in our system"
+            )
+
+        if not user.check_password(form.password.data):
+            return render_template("auth/login.html", form=form, error="Wrong password")
+
+        if not user.activated:
+            return render_template(
+                "auth/login.html",
+                form=form,
+                show_resend_activation=True,
+                error="Please check your inbox for the activation email. You can also have this email re-sent",
+            )
+
+        LOG.debug("log user %s in", user)
+        login_user(user)
+
+        # User comes to login page from another page
+        if "next" in request.args:
+            next_url = request.args.get("next")
+            LOG.debug("redirect user to %s", next_url)
+            return redirect(next_url)
+        else:
+            LOG.debug("redirect user to dashboard")
             return redirect(url_for("dashboard.index"))
 
     return render_template("auth/login.html", form=form)

+ 89 - 0
app/auth/views/register.py

@@ -0,0 +1,89 @@
+import arrow
+from flask import request, flash, render_template
+from flask_wtf import FlaskForm
+from wtforms import StringField, validators
+
+from app import email_utils
+from app.auth.base import auth_bp
+from app.config import URL
+from app.email_utils import notify_admin
+from app.extensions import db
+from app.log import LOG
+from app.models import User, ActivationCode, PlanEnum, GenEmail
+from app.utils import random_string, encode_url
+
+
+class RegisterForm(FlaskForm):
+    email = StringField("Email", validators=[validators.DataRequired()])
+    password = StringField(
+        "Password", validators=[validators.DataRequired(), validators.Length(min=8)]
+    )
+    name = StringField("Name", validators=[validators.DataRequired()])
+
+
+@auth_bp.route("/register", methods=["GET", "POST"])
+def register():
+    form = RegisterForm(request.form)
+
+    if form.validate_on_submit():
+        user = User.filter_by(email=form.email.data).first()
+
+        if user:
+            flash(f"Email {form.email.data} already exists", "warning")
+            return render_template("auth/register.html", form=form)
+
+        LOG.debug("create user %s", form.email.data)
+        user = User.create(email=form.email.data, name=form.name.data)
+        user.set_password(form.password.data)
+
+        # by default new user will be trial period
+        user.plan = PlanEnum.trial
+        user.plan_expiration = arrow.now().shift(days=+15)
+        db.session.flush()
+
+        # create a first alias mail to show user how to use when they login
+        GenEmail.create_new_gen_email(user_id=user.id)
+        db.session.commit()
+
+        send_activation_email(user)
+        notify_admin(
+            f"new user signs up {user.email}", f"{user.name} signs up at {arrow.now()}"
+        )
+
+        return render_template("auth/register_waiting_activation.html")
+
+    return render_template("auth/register.html", form=form)
+
+
+def send_activation_email(user):
+    activation = ActivationCode.create(user_id=user.id, code=random_string(30))
+    db.session.commit()
+
+    # Send user activation email
+    activation_link = f"{URL}/auth/activate?code={activation.code}"
+    if "next" in request.args:
+        LOG.d("redirect user to %s after activation", request.args["next"])
+        activation_link = activation_link + "&next=" + encode_url(request.args["next"])
+
+    email_utils.send(
+        user.email,
+        f"Welcome to SimpleLogin {user.name} - just one more step!",
+        html_content=f"""
+                Welcome to SimpleLogin! <br><br>
+
+Our mission is to make the login process as smooth and as secure as possible. This should be easy. <br><br>
+
+To get started, we need to confirm your email address, so please click this <a href="{activation_link}">link</a> 
+to finish creating your account. Or you can paste this link into your browser: <br><br>
+
+{activation_link} <br><br>
+
+Your feedbacks are very important to us. Please feel free to reply to this email to let us know any 
+of your suggestion! <br><br>
+
+Thanks! <br><br>
+
+SimpleLogin team.
+            
+            """,
+    )

+ 39 - 0
app/auth/views/resend_activation.py

@@ -0,0 +1,39 @@
+from flask import request, flash, render_template, redirect, url_for
+from flask_wtf import FlaskForm
+from wtforms import StringField, validators
+
+from app.auth.base import auth_bp
+from app.auth.views.register import send_activation_email
+from app.log import LOG
+from app.models import User
+
+
+class ResendActivationForm(FlaskForm):
+    email = StringField("Email", validators=[validators.DataRequired()])
+
+
+@auth_bp.route("/resend_activation", methods=["GET", "POST"])
+def resend_activation():
+    form = ResendActivationForm(request.form)
+
+    if form.validate_on_submit():
+        user = User.filter_by(email=form.email.data).first()
+
+        if not user:
+            flash("There's no such email", "warning")
+            return render_template("auth/resend_activation.html", form=form)
+
+        if user.activated:
+            flash("your account is already activated, please login", "success")
+            return redirect(url_for("auth.login"))
+
+        # user is not activated
+        LOG.d("user %s is not activated", user)
+        flash(
+            "An activation email is on its way, please check your inbox/spam folder",
+            "warning",
+        )
+        send_activation_email(user)
+        return render_template("auth/register_waiting_activation.html")
+
+    return render_template("auth/resend_activation.html", form=form)

+ 59 - 0
app/auth/views/reset_password.py

@@ -0,0 +1,59 @@
+import arrow
+from flask import request, flash, render_template, redirect, url_for
+from flask_login import login_user
+from flask_wtf import FlaskForm
+from wtforms import StringField, validators
+
+from app.auth.base import auth_bp
+from app.extensions import db
+from app.models import ResetPasswordCode
+
+
+class ResetPasswordForm(FlaskForm):
+    password = StringField(
+        "Password", validators=[validators.DataRequired(), validators.Length(min=8)]
+    )
+
+
+@auth_bp.route("/reset_password", methods=["GET", "POST"])
+def reset_password():
+    form = ResetPasswordForm(request.form)
+
+    reset_password_code_str = request.args.get("code")
+
+    reset_password_code: ResetPasswordCode = ResetPasswordCode.get_by(
+        code=reset_password_code_str
+    )
+
+    if not reset_password_code:
+        error = (
+            "The reset password link can be used only once. "
+            "Please make a new request to reset password"
+        )
+        return render_template("auth/reset_password.html", form=form, error=error)
+
+    if reset_password_code.expired < arrow.now():
+        error = (
+            "The link is already expired. Please make a new request to reset password"
+        )
+        return render_template("auth/reset_password.html", form=form, error=error)
+
+    if form.validate_on_submit():
+        user = reset_password_code.user
+
+        user.set_password(form.password.data)
+
+        flash("Your new password has been set", "success")
+
+        # this can be served to activate user too
+        user.activated = True
+
+        # remove the reset password code
+        reset_password_code.delete()
+
+        db.session.commit()
+        login_user(user)
+
+        return redirect(url_for("dashboard.index"))
+
+    return render_template("auth/reset_password.html", form=form)

+ 59 - 0
app/config.py

@@ -0,0 +1,59 @@
+import os
+import subprocess
+
+from dotenv import load_dotenv
+
+SHA1 = subprocess.getoutput("git rev-parse HEAD")
+
+config_file = os.environ.get("CONFIG")
+if config_file:
+    print("load config file", config_file)
+    load_dotenv(config_file)
+else:
+    load_dotenv()
+
+
+URL = os.environ.get("URL") or "http://sl-server:5000"
+EMAIL_DOMAIN = os.environ.get("EMAIL_DOMAIN") or "sl"
+SUPPORT_EMAIL = os.environ.get("SUPPORT_EMAIL") or "support@sl"
+SENDGRID_API_KEY = os.environ.get("SENDGRID_API_KEY")
+DB_URI = os.environ.get("DB_URI") or "sqlite:///db.sqlite"
+
+FLASK_SECRET = os.environ.get("FLASK_SECRET") or "secret"
+
+# invalidate the session at each new version by changing the secret
+FLASK_SECRET = FLASK_SECRET + SHA1
+
+ENABLE_SENTRY = "ENABLE_SENTRY" in os.environ
+ENV = os.environ.get("ENV")
+
+print("email domain is", EMAIL_DOMAIN)
+
+
+AWS_REGION = "eu-west-3"
+BUCKET = os.environ.get("BUCKET") or "local.sl"
+AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID")
+AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY")
+
+ENABLE_CLOUDWATCH = "ENABLE_CLOUDWATCH" in os.environ
+CLOUDWATCH_LOG_GROUP = os.environ.get("CLOUDWATCH_LOG_GROUP")
+CLOUDWATCH_LOG_STREAM = os.environ.get("CLOUDWATCH_LOG_STREAM")
+
+STRIPE_API = os.environ.get("STRIPE_API")  # Stripe public key
+STRIPE_SECRET_KEY = os.environ.get("STRIPE_SECRET_KEY")
+STRIPE_YEARLY_PLAN = os.environ.get("STRIPE_YEARLY_PLAN")
+STRIPE_MONTHLY_PLAN = os.environ.get("STRIPE_MONTHLY_PLAN")
+
+# Max number emails user can generate for free plan
+MAX_NB_EMAIL_FREE_PLAN = int(os.environ.get("MAX_NB_EMAIL_FREE_PLAN"))
+
+LYRA_ANALYTICS_ID = os.environ.get("LYRA_ANALYTICS_ID")
+
+# Used to sign id_token
+OPENID_PRIVATE_KEY_PATH = os.environ.get("OPENID_PRIVATE_KEY_PATH")
+OPENID_PUBLIC_KEY_PATH = os.environ.get("OPENID_PUBLIC_KEY_PATH")
+
+PARTNER_CODES = ["SL2019"]
+
+# Allow user to have 1 year of premium: set the expiration_date to 1 year more
+PROMO_CODE = "SIMPLEISBETTER"

+ 1 - 1
app/dashboard/__init__.py

@@ -1 +1 @@
-from .views import index
+from .views import index, pricing, setting

+ 4 - 1
app/dashboard/base.py

@@ -1,5 +1,8 @@
 from flask import Blueprint
 
 dashboard_bp = Blueprint(
-    name="dashboard", import_name=__name__, url_prefix="/dashboard"
+    name="dashboard",
+    import_name=__name__,
+    url_prefix="/dashboard",
+    template_folder="templates",
 )

+ 244 - 0
app/dashboard/templates/dashboard/index.html

@@ -0,0 +1,244 @@
+{% extends 'default.html' %}
+
+{% set active_page = "dashboard" %}
+
+{% block title %}
+  Dashboard
+{% endblock %}
+
+{% block default_content %}
+  <div class="page-header row">
+    <h3 class="page-title col"
+        data-intro="Here, you find the list of all <b>email alias</b> created. <br><br>
+        Emails sent to an <b>alias</b> will be forwarded to your personal email. <br><br>
+        Please note that email alias is <b>NOT</b> temporary, meaning an alias works forever! <br><br>
+        Email alias is a great way to hide your personal email so feel free to
+        use it whenever possible, for ex on untrusted websites.">
+      Email Alias
+    </h3>
+    <form method="post" class="col text-right">
+      <input type="hidden" name="form-name" value="create-new-email">
+      <button class="btn btn-success">Create email alias</button>
+    </form>
+  </div>
+
+  <div class="row row-cards row-deck mt-4">
+    <div class="col-12">
+      <div class="card">
+        <div class="table-responsive">
+          <table class="table table-hover table-outline table-vcenter text-nowrap card-table">
+            <thead>
+            <tr>
+              <th>Email</th>
+              <th>
+                Used On
+                <i class="fe fe-help-circle" data-toggle="tooltip"
+                   title="List of app/website that has received this email"></i>
+              </th>
+              <th>Actions</th>
+              <th>
+                Enable/Disable Email Forwarding
+              </th>
+              <th>Created At</th>
+            </tr>
+            </thead>
+            <tbody>
+            {% for gen_email in gen_emails %}
+              <tr>
+                <td>
+                  <div>
+                    <a href="mailto: {{ gen_email.email }}">{{ gen_email.email }}</a>
+                  </div>
+                </td>
+
+                <td>
+                  {% for client_user in gen_email.client_users %}
+                    {{ client_user.client.name }} <br>
+                  {% endfor %}
+                </td>
+
+                <td>
+                  <div class="btn-group">
+                    <button class="clipboard btn btn-secondary btn-sm"
+                            data-clipboard-text="{{ gen_email.email }}">
+                      Copy
+                    </button>
+
+                    <form method="post">
+                      <input type="hidden" name="form-name" value="trigger-email">
+                      <input type="hidden" name="gen-email-id" value="{{ gen_email.id }}">
+
+                      {% if gen_email.enabled %}
+                        <button class="btn btn-secondary btn-sm"
+                            {% if loop.index ==1 %}
+                                data-intro="By triggering the test email, 
+                            SimpleLogin server will send an email to this alias
+                            and this email should arrive to your personal email"
+                            {% endif %}
+                        >Trigger Test Email
+                        </button>
+                      {% endif %}
+                    </form>
+                  </div>
+                </td>
+
+                <td>
+                  <form method="post">
+                    <input type="hidden" name="form-name" value="switch-email-forwarding">
+                    <input type="hidden" name="gen-email-id" value="{{ gen_email.id }}">
+
+                    <label class="custom-switch"
+                        {% if loop.index ==1 %}
+                           data-intro="By turning off an alias, emails sent to this alias will <b>NOT</b>
+                           be forwarded to your personal email. <br><br>
+                           This should only be used with care as others might
+                           not be able to reach you after ...
+                            "
+                        {% endif %}
+                    >
+                      <input type="checkbox" class="custom-switch-input"
+                          {{ "checked" if gen_email.enabled else "" }}>
+                      <span class="custom-switch-indicator"></span>
+                    </label>
+                  </form>
+                </td>
+
+                <td>
+                  {{ gen_email.created_at | dt }}
+                </td>
+              </tr>
+            {% endfor %}
+
+            </tbody>
+          </table>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <div class="page-header row">
+    <h3 class="page-title col" data-intro="Here you can find the list of website/app on which
+    you have used the <b>Connect with SimpleLogin</b> button <br><br>
+    You also see what information that SimpleLogin has communicated to these website/app when you sign in.
+">
+      Apps
+    </h3>
+  </div>
+
+  <div class="row row-cards row-deck mt-4">
+    <div class="col-12">
+      <div class="card">
+        <div class="table-responsive">
+          <table class="table table-hover table-outline table-vcenter text-nowrap card-table">
+            <thead>
+            <tr>
+              <th>
+                App
+              </th>
+              <th>
+                Information
+                <i class="fe fe-help-circle" data-toggle="tooltip"
+                   title="Information sent to this app/website"></i>
+              </th>
+              <th class="text-center">
+                First used
+                <i class="fe fe-help-circle" data-toggle="tooltip"
+                   title="The first time you have used the SimpleLogin on this app/website"></i>
+              </th>
+              <!--<th class="text-center">Last used</th>-->
+            </tr>
+            </thead>
+            <tbody>
+            {% for client_user in client_users %}
+              <tr>
+                <td>
+                  {{ client_user.client.name }}
+                </td>
+
+                <td>
+                  {% for scope, val in client_user.get_user_info().items() %}
+                    <div>
+                      {% if scope == "email" %}
+                        Email: <a href="mailto:{{ val }}">{{ val }}</a>
+                      {% elif scope == "name" %}
+                        Name: {{ val }}
+                      {% endif %}
+                    </div>
+                  {% endfor %}
+                </td>
+
+
+                <td class="text-center">
+                  {{ client_user.created_at | dt }}
+                </td>
+
+                {#            TODO: add last_used#}
+                <!--
+                  <td class="text-center">
+                    <div>4 minutes ago</div>
+                  </td>
+                -->
+
+              </tr>
+            {% endfor %}
+
+            </tbody>
+          </table>
+        </div>
+      </div>
+    </div>
+  </div>
+{% endblock %}
+
+{% block script %}
+  <script>
+    require(['clipboard', 'notie', 'jquery', 'intro'], function (Clipboard, notie, $, intro) {
+      var clipboard = new Clipboard('.clipboard');
+
+      var introShown = localStorage.getItem("introShown");
+      console.log(introShown);
+      if ("yes" !== introShown) {
+        intro().start();
+        localStorage.setItem("introShown", "yes")
+      }
+
+      clipboard.on('success', function (e) {
+        notie.alert({
+          type: "success",
+          text: "Copied to clipboard",
+          time: 1,
+        });
+
+        e.clearSelection();
+      });
+
+      // the modal does not get close when user clicks outside of modal
+      // necessary for obligatory modal such as the one displayed when user enable/display email forwarding
+      notie.setOptions({
+        overlayClickDismiss: false,
+      });
+
+      $(".custom-switch-input").change(function (e) {
+        var message = "";
+
+        if (e.target.checked) {
+          message = `After this, you will start receiving email sent to this email address, please confirm`;
+        } else {
+          message = `After this, you will stop receiving email sent to this email address, please confirm`;
+        }
+
+        notie.confirm({
+          text: message,
+          cancelCallback: () => {
+            // reset to the original value
+            var oldValue = !$(this).prop("checked");
+            $(this).prop("checked", oldValue);
+          },
+          submitCallback: () => {
+            $(this).closest("form").submit();
+          }
+        });
+      })
+    })
+  </script>
+{% endblock %}

+ 182 - 0
app/dashboard/templates/dashboard/pricing.html

@@ -0,0 +1,182 @@
+{% extends 'default.html' %}
+
+{% set active_page = "dashboard" %}
+
+{% block title %}
+  Pricing
+{% endblock %}
+
+{% block head %}
+  <style type="text/css">
+    /**
+   * The CSS shown here will not be introduced in the Quickstart guide, but shows
+   * how you can use CSS to style your Element's container.
+   */
+    .StripeElement {
+      box-sizing: border-box;
+
+      height: 40px;
+
+      padding: 10px 12px;
+
+      border: 1px solid transparent;
+      border-radius: 4px;
+      background-color: white;
+
+      box-shadow: 0 1px 3px 0 #e6ebf1;
+      -webkit-transition: box-shadow 150ms ease;
+      transition: box-shadow 150ms ease;
+    }
+
+    .StripeElement--focus {
+      box-shadow: 0 1px 3px 0 #cfd7df;
+    }
+
+    .StripeElement--invalid {
+      border-color: #fa755a;
+    }
+
+    .StripeElement--webkit-autofill {
+      background-color: #fefde5 !important;
+    }
+  </style>
+{% endblock %}
+
+{% block default_content %}
+  <script src="https://js.stripe.com/v3/"></script>
+
+  <div class="row">
+    <div class="col-sm-6 col-lg-6">
+      <div class="card">
+        <div class="card-body text-center">
+          <div class="card-category">Premium</div>
+          <div class="display-4 my-6">$10/year</div>
+          <div class="display-5 my-6">or</div>
+          <div class="display-4 my-6">$1/month</div>
+          <ul class="list-unstyled leading-loose">
+            <li><i class="fe fe-check text-success mr-2" aria-hidden="true"></i> Privacy protected</li>
+            <li><i class="fe fe-check text-success mr-2" aria-hidden="true"></i> Infinite Login</li>
+            <li><i class="fe fe-check text-success mr-2" aria-hidden="true"></i> Infinite Emails</li>
+            <li><i class="fe fe-check text-success mr-2" aria-hidden="true"></i>
+              Support us and our application partners
+            </li>
+          </ul>
+        </div>
+      </div>
+    </div>
+
+    <div class="col-sm-6 col-lg-6">
+      <div class="display-6">
+        The payment is processed by <a href="https://stripe.com" target="_blank">Stripe</a>. <br>
+        Your card number is never stored on our server.
+      </div>
+
+      <hr>
+
+      <form method="post" id="payment-form">
+        <div class="form-group">
+          <label for="card-element" class="form-label">
+            Credit or debit card
+          </label>
+          <div id="card-element">
+            <!-- A Stripe Element will be inserted here. -->
+          </div>
+
+          <!-- Used to display form errors. -->
+          <div id="card-errors" role="alert" class="text-danger"></div>
+        </div>
+
+        <div class="form-group">
+          <div class="form-label">Plan</div>
+          <div class="custom-controls-stacked">
+            <label class="custom-control custom-radio custom-control-inline">
+              <input type="radio" class="custom-control-input" name="plan" value="yearly" checked>
+              <span class="custom-control-label">Yearly</span>
+            </label>
+            <label class="custom-control custom-radio custom-control-inline">
+              <input type="radio" class="custom-control-input" name="plan" value="monthly">
+              <span class="custom-control-label">Monthly</span>
+            </label>
+          </div>
+        </div>
+
+        <button type="submit" class="btn btn-success">Upgrade</button>
+      </form>
+    </div>
+  </div>
+
+  <script>
+    // Create a Stripe client.
+    var stripe = Stripe('{{ stripe_api }}');
+
+    // Create an instance of Elements.
+    var elements = stripe.elements();
+
+    // Custom styling can be passed to options when creating an Element.
+    // (Note that this demo uses a wider set of styles than the guide below.)
+    var style = {
+      base: {
+        color: '#32325d',
+        fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
+        fontSmoothing: 'antialiased',
+        fontSize: '16px',
+        '::placeholder': {
+          color: '#aab7c4'
+        }
+      },
+      invalid: {
+        color: '#fa755a',
+        iconColor: '#fa755a'
+      }
+    };
+
+    // Create an instance of the card Element.
+    // the postal code is asked on Safari but not on other browsers ...
+    // Disable it explicitly
+    var card = elements.create('card', {hidePostalCode: true, style: style});
+
+    // Add an instance of the card Element into the `card-element` <div>.
+    card.mount('#card-element');
+
+    // Handle real-time validation errors from the card Element.
+    card.addEventListener('change', function (event) {
+      var displayError = document.getElementById('card-errors');
+      if (event.error) {
+        displayError.textContent = event.error.message;
+      } else {
+        displayError.textContent = '';
+      }
+    });
+
+    // Handle form submission.
+    var form = document.getElementById('payment-form');
+    form.addEventListener('submit', function (event) {
+      event.preventDefault();
+
+      stripe.createToken(card).then(function (result) {
+        if (result.error) {
+          // Inform the user if there was an error.
+          var errorElement = document.getElementById('card-errors');
+          errorElement.textContent = result.error.message;
+        } else {
+          // Send the token to your server.
+          stripeTokenHandler(result.token);
+        }
+      });
+    });
+
+    // Submit the form with the token ID.
+    function stripeTokenHandler(token) {
+      // Insert the token ID into the form so it gets submitted to the server
+      var form = document.getElementById('payment-form');
+      var hiddenInput = document.createElement('input');
+      hiddenInput.setAttribute('type', 'hidden');
+      hiddenInput.setAttribute('name', 'stripeToken');
+      hiddenInput.setAttribute('value', token.id);
+      form.appendChild(hiddenInput);
+
+      // Submit the form
+      form.submit();
+    }
+  </script>
+{% endblock %}

+ 101 - 0
app/dashboard/templates/dashboard/setting.html

@@ -0,0 +1,101 @@
+{% from "_formhelpers.html" import render_field, render_field_errors %}
+
+{% extends 'default.html' %}
+
+{% set active_page = "dashboard" %}
+
+{% block title %}
+  Setting
+{% endblock %}
+
+{% block default_content %}
+
+  <div class="col-md-8 offset-md-2">
+    <form method="post" enctype="multipart/form-data">
+      {{ form.csrf_token }}
+      <input type="hidden" name="form-name" value="update-profile">
+
+      <h3>Profile</h3>
+      <div class="form-group">
+        <label class="form-label">Name</label>
+        {{ form.name(class="form-control", value=current_user.name) }}
+        {{ render_field_errors(form.name) }}
+      </div>
+
+      <div class="form-group">
+        <div class="form-label">Profile picture</div>
+        {{ form.profile_picture(class="form-control-file") }}
+        {{ render_field_errors(form.profile_picture) }}
+
+        <img src="{{ current_user.profile_picture_url() }}" class="profile-picture">
+      </div>
+
+      <button class="btn btn-primary">Update</button>
+    </form>
+
+    <hr>
+    <h3>Current subscription</h3>
+    Your current plan is
+    {% if current_user.is_premium() %}
+      <b>{{ current_user.plan.name }}</b>
+      <br>
+      {% if current_user.plan_expiration %}
+        Ends {{ current_user.plan_expiration.humanize() }}
+      {% else %}
+        Renewed {{ current_user.plan_current_period_end().humanize() }}
+      {% endif %}
+    {% else %}
+      <b>{{ current_user.plan.name }}</b><br>
+      {% if current_user.plan == PlanEnum.trial %}
+        Ends {{ current_user.plan_expiration.humanize() }}<br>
+      {% endif %}
+      <a href="{{ url_for('dashboard.pricing') }}" class="btn btn-sm btn-outline-primary">
+        Upgrade To Premium
+      </a>
+      <br><br>
+      <form method="post">
+        {{ promo_form.csrf_token }}
+        <input type="hidden" name="form-name" value="promo-code">
+        <h5>If you have a promo code, you can enter it here</h5>
+        <p class="text-muted">You can use a given promo code only once :)</p>
+        <div class="form-group">
+          <label class="form-label">Promo code</label>
+          {{ promo_form.code(class="form-control") }}
+          {{ render_field_errors(promo_form.code) }}
+        </div>
+        <button class="btn btn-primary">Apply</button>
+      </form>
+    {% endif %}
+
+    {% if current_user.is_premium() %}
+      <!-- This corresponds to the more rare case where user has upgraded the plan,
+      downgraded it and decides to upgrade again before the end of the previous plan  -->
+      {% if current_user.plan_expiration %}
+        <form method="post">
+          <input type="hidden" name="form-name" value="reactivate-subscription">
+          <br><br>
+          <button class="btn btn-warning">Reactivate subscription</button>
+        </form>
+      {% else %}
+        <!-- current_user.plan_expiration=None, this corresponds to the usual case
+        where user has upgraded the plan, and now decide to downgrade it. -->
+        <form method="post"
+              onsubmit="return confirm('Your plan will be downgraded to free plan {{ current_user.plan_current_period_end().humanize() }}, please confirm.')">
+          <input type="hidden" name="form-name" value="cancel-subscription">
+          <br><br>
+          <button class="btn btn-warning">Cancel subscription</button>
+        </form>
+      {% endif %}
+    {% endif %}
+
+    <hr>
+    <h3>Change password</h3>
+    <form method="post">
+      <input type="hidden" name="form-name" value="change-password">
+      <button class="btn btn-outline-primary">Change password</button>
+    </form>
+
+  </div>
+
+{% endblock %}
+

+ 83 - 4
app/dashboard/views/index.py

@@ -1,10 +1,89 @@
-from flask import render_template
-from flask_login import login_required
+from flask import render_template, request, redirect, url_for, flash
+from flask_login import login_required, current_user
+from sqlalchemy.orm import joinedload
 
+from app import email_utils
 from app.dashboard.base import dashboard_bp
+from app.extensions import db
+from app.log import LOG
+from app.models import GenEmail, ClientUser
 
 
-@dashboard_bp.route("/")
+@dashboard_bp.route("/", methods=["GET", "POST"])
 @login_required
 def index():
-    return render_template("dashboard/index.html")
+    # User generates a new email
+    if request.method == "POST":
+        if request.form.get("form-name") == "trigger-email":
+            gen_email_id = request.form.get("gen-email-id")
+            gen_email = GenEmail.get(gen_email_id)
+
+            LOG.d("trigger an email to %s", gen_email)
+            email_utils.send(
+                gen_email.email,
+                "A Test Email",
+                f"""
+Hi {current_user.name} ! <br><br>
+This is a test email to make sure you receive email sent at {gen_email.email} <br><br>
+If you have any question, feel free to reply to this email :) <br><br>
+Have a nice day <br><br>
+SimpleLogin team.
+            """,
+            )
+            flash(
+                f"An email sent to {gen_email.email} is on its way, please check your inbox/spam folder",
+                "success",
+            )
+
+        elif request.form.get("form-name") == "create-new-email":
+            can_create_new_email = current_user.can_create_new_email()
+
+            if can_create_new_email:
+                gen_email = GenEmail.create_new_gen_email(user_id=current_user.id)
+                db.session.commit()
+
+                LOG.d("generate new email %s for user %s", gen_email, current_user)
+                flash(f"Email {gen_email.email} has been created", "success")
+            else:
+                flash(f"You need to upgrade your plan to create new email.", "warning")
+
+        elif request.form.get("form-name") == "switch-email-forwarding":
+            gen_email_id = request.form.get("gen-email-id")
+            gen_email: GenEmail = GenEmail.get(gen_email_id)
+
+            LOG.d("switch email forwarding for %s", gen_email)
+
+            gen_email.enabled = not gen_email.enabled
+            if gen_email.enabled:
+                flash(
+                    f"The email forwarding for {gen_email.email} has been enabled",
+                    "success",
+                )
+            else:
+                flash(
+                    f"The email forwarding for {gen_email.email} has been disabled",
+                    "warning",
+                )
+            db.session.commit()
+
+        return redirect(url_for("dashboard.index"))
+
+    client_users = (
+        ClientUser.filter_by(user_id=current_user.id)
+        .options(joinedload(ClientUser.client))
+        .options(joinedload(ClientUser.gen_email))
+        .all()
+    )
+
+    sorted(client_users, key=lambda cu: cu.client.name)
+
+    gen_emails = (
+        GenEmail.filter_by(user_id=current_user.id)
+        .order_by(GenEmail.email)
+        .options(joinedload(GenEmail.client_users))
+        .all()
+    )
+
+    return render_template(
+        "dashboard/index.html", client_users=client_users, gen_emails=gen_emails
+    )

+ 90 - 0
app/dashboard/views/pricing.py

@@ -0,0 +1,90 @@
+import stripe
+from flask import render_template, request, flash, redirect, url_for
+from flask_login import login_required, current_user
+from stripe.error import CardError
+
+from app.config import STRIPE_API, STRIPE_MONTHLY_PLAN, STRIPE_YEARLY_PLAN
+from app.dashboard.base import dashboard_bp
+from app.email_utils import notify_admin
+from app.extensions import db
+from app.log import LOG
+from app.models import PlanEnum
+
+
+@dashboard_bp.route("/pricing", methods=["GET", "POST"])
+@login_required
+def pricing():
+    # sanity check: make sure this page is only for free user that has never subscribed before
+    # case user unsubscribe and re-subscribe will be handled later
+    if current_user.is_premium():
+        flash("You are already a premium user", "warning")
+        return redirect(url_for("dashboard.index"))
+
+    if (
+        current_user.stripe_customer_id
+        or current_user.stripe_card_token
+        or current_user.stripe_subscription_id
+    ):
+        raise Exception("only user not exist on stripe can view this page")
+
+    if stripe.Customer.list(email=current_user.email):
+        raise Exception("user email is already used on stripe!")
+
+    if request.method == "POST":
+        plan_str = request.form.get("plan")  # either monthly or yearly
+        if plan_str == "monthly":
+            plan = PlanEnum.monthly
+        elif plan_str == "yearly":
+            plan = PlanEnum.yearly
+        else:
+            raise Exception("Plan must be either yearly or monthly")
+
+        stripe_token = request.form.get("stripeToken")
+        LOG.d("stripe card token %s for plan %s", stripe_token, plan)
+        current_user.stripe_card_token = stripe_token
+
+        try:
+            customer = stripe.Customer.create(
+                source=stripe_token,
+                email=current_user.email,
+                metadata={"id": current_user.id},
+                name=current_user.name,
+            )
+        except CardError as e:
+            LOG.exception("payment problem, code:%s", e.code)
+            flash(
+                "Payment refused with error {e.message}. Could you re-try with another card please?",
+                "danger",
+            )
+        else:
+            LOG.d("stripe customer %s", customer)
+            current_user.stripe_customer_id = customer.id
+
+            stripe_plan = (
+                STRIPE_MONTHLY_PLAN if plan == PlanEnum.monthly else STRIPE_YEARLY_PLAN
+            )
+            subscription = stripe.Subscription.create(
+                customer=current_user.stripe_customer_id,
+                items=[{"plan": stripe_plan}],
+                expand=["latest_invoice.payment_intent"],
+            )
+
+            LOG.d("stripe subscription %s", subscription)
+
+            current_user.stripe_subscription_id = subscription.id
+
+            db.session.commit()
+
+            if subscription.latest_invoice.payment_intent.status == "succeeded":
+                LOG.d("payment successful for user %s", current_user)
+                current_user.plan = plan
+                current_user.plan_expiration = None
+                db.session.commit()
+                flash("Thanks for your subscription!", "success")
+                notify_admin(
+                    f"user {current_user.email} has finished subscription",
+                    f"plan: {plan}",
+                )
+                return redirect(url_for("dashboard.index"))
+
+    return render_template("dashboard/pricing.html", stripe_api=STRIPE_API)

+ 173 - 0
app/dashboard/views/setting.py

@@ -0,0 +1,173 @@
+from io import BytesIO
+
+import arrow
+import stripe
+from flask import render_template, request, redirect, url_for, flash
+from flask_login import login_required, current_user
+from flask_wtf import FlaskForm
+from flask_wtf.file import FileField
+from wtforms import StringField, validators
+
+from app import s3, email_utils
+from app.config import URL, PROMO_CODE
+from app.dashboard.base import dashboard_bp
+from app.email_utils import notify_admin
+from app.extensions import db
+from app.log import LOG
+from app.models import PlanEnum, File, ResetPasswordCode
+from app.utils import random_string
+
+
+class SettingForm(FlaskForm):
+    name = StringField("Name", validators=[validators.DataRequired()])
+    profile_picture = FileField("Profile Picture")
+
+
+class PromoCodeForm(FlaskForm):
+    code = StringField("Name", validators=[validators.DataRequired()])
+
+
+@dashboard_bp.route("/setting", methods=["GET", "POST"])
+@login_required
+def setting():
+    form = SettingForm()
+    promo_form = PromoCodeForm()
+
+    if request.method == "POST":
+        if request.form.get("form-name") == "update-profile":
+            if form.validate():
+                # update user info
+                current_user.name = form.name.data
+
+                if form.profile_picture.data:
+                    file_path = random_string(30)
+                    file = File.create(path=file_path)
+
+                    s3.upload_from_bytesio(
+                        file_path, BytesIO(form.profile_picture.data.read())
+                    )
+
+                    db.session.flush()
+                    LOG.d("upload file %s to s3", file)
+
+                    current_user.profile_picture_id = file.id
+                    db.session.flush()
+
+                db.session.commit()
+                flash(f"Your profile has been updated", "success")
+        elif request.form.get("form-name") == "cancel-subscription":
+            # sanity check
+            if not (current_user.is_premium() and current_user.plan_expiration is None):
+                raise Exception("user cannot cancel subscription")
+
+            notify_admin(f"user {current_user} cancels subscription")
+
+            # the plan will finish at the end of the current period
+            current_user.plan_expiration = current_user.plan_current_period_end()
+            stripe.Subscription.modify(
+                current_user.stripe_subscription_id, cancel_at_period_end=True
+            )
+            db.session.commit()
+            flash(
+                f"Your plan will be downgraded {current_user.plan_expiration.humanize()}",
+                "success",
+            )
+        elif request.form.get("form-name") == "reactivate-subscription":
+            if not (current_user.is_premium() and current_user.plan_expiration):
+                raise Exception("user cannot reactivate subscription")
+
+            notify_admin(f"user {current_user} reactivates subscription")
+
+            # the plan will finish at the end of the current period
+            current_user.plan_expiration = None
+            stripe.Subscription.modify(
+                current_user.stripe_subscription_id, cancel_at_period_end=False
+            )
+            db.session.commit()
+            flash(f"Your plan is reactivated now, thank you!", "success")
+        elif request.form.get("form-name") == "change-password":
+            send_reset_password_email(current_user)
+        elif request.form.get("form-name") == "promo-code":
+            if promo_form.validate():
+                promo_code = promo_form.code.data.upper()
+                if promo_code != PROMO_CODE:
+                    flash(
+                        "Unknown promo code. Are you sure this is the right code?",
+                        "warning",
+                    )
+                    return render_template(
+                        "dashboard/setting.html",
+                        form=form,
+                        PlanEnum=PlanEnum,
+                        promo_form=promo_form,
+                    )
+                elif promo_code in current_user.get_promo_codes():
+                    flash(
+                        "You have already used this promo code. A code can be used only once :(",
+                        "warning",
+                    )
+                    return render_template(
+                        "dashboard/setting.html",
+                        form=form,
+                        PlanEnum=PlanEnum,
+                        promo_form=promo_form,
+                    )
+                else:
+                    LOG.d("apply promo code %s for user %s", promo_code, current_user)
+                    current_user.plan = PlanEnum.trial
+
+                    if current_user.plan_expiration:
+                        LOG.d("extend the current plan 1 year")
+                        current_user.plan_expiration = current_user.plan_expiration.shift(
+                            years=1
+                        )
+                    else:
+                        LOG.d("set plan_expiration to 1 year from now")
+                        current_user.plan_expiration = arrow.now().shift(years=1)
+
+                    current_user.save_new_promo_code(promo_code)
+                    db.session.commit()
+
+                    flash(
+                        "The promo code has been applied successfully to your account!",
+                        "success",
+                    )
+
+        return redirect(url_for("dashboard.setting"))
+
+    return render_template(
+        "dashboard/setting.html", form=form, PlanEnum=PlanEnum, promo_form=promo_form
+    )
+
+
+def send_reset_password_email(user):
+    """
+    generate a new ResetPasswordCode and send it over email to user
+    """
+    reset_password_code = ResetPasswordCode.create(
+        user_id=user.id, code=random_string(60)
+    )
+    db.session.commit()
+
+    reset_password_link = f"{URL}/auth/reset_password?code={reset_password_code.code}"
+
+    email_utils.send(
+        user.email,
+        f"Reset your password on SimpleLogin",
+        html_content=f"""
+    Hi {user.name}! <br><br>
+
+    To reset or change your password, please follow this link <a href="{reset_password_link}">reset password</a>. 
+    Or you can paste this link into your browser: <br><br>
+
+    {reset_password_link} <br><br>
+
+    Cheers,
+    SimpleLogin team.
+    """,
+    )
+
+    flash(
+        "You are going to receive an email containing instruction to change your password",
+        "success",
+    )

+ 1 - 0
app/developer/__init__.py

@@ -0,0 +1 @@
+from .views import index, new_client, client_detail

+ 18 - 0
app/developer/base.py

@@ -0,0 +1,18 @@
+from flask import Blueprint, render_template
+from flask_login import current_user
+
+from app.log import LOG
+
+developer_bp = Blueprint(
+    name="developer",
+    import_name=__name__,
+    url_prefix="/developer",
+    template_folder="templates",
+)
+
+
+@developer_bp.before_request
+def before_request():
+    if current_user.is_authenticated and not current_user.is_developer:
+        LOG.error("User %s tries to go developer tab")
+        return render_template("error/403.html"), 403

+ 150 - 0
app/developer/templates/developer/client_detail.html

@@ -0,0 +1,150 @@
+{% from "_formhelpers.html" import render_field, render_field_errors %}
+
+{% extends 'default.html' %}
+
+{% set active_page = "developer" %}
+
+{% block title %}
+  Developer - Edit client
+{% endblock %}
+
+{% block default_content %}
+  <div class="col-md-8 offset-md-2">
+    <form method="post" enctype="multipart/form-data">
+      {{ form.csrf_token }}
+
+      <h3>App Information</h3>
+      <div class="form-group">
+        <label class="form-label">App Name</label>
+        {{ form.name(class="form-control", value=client.name) }}
+        {{ render_field_errors(form.name) }}
+      </div>
+
+      <div class="form-group">
+        <label class="form-label">Website Url</label>
+        {{ form.home_url(class="form-control", type="url", value=client.home_url or "") }}
+        {{ render_field_errors(form.home_url) }}
+      </div>
+
+
+      <div class="form-group">
+        <div class="form-label">App Icon</div>
+        {{ form.icon(class="form-control-file") }}
+        {{ render_field_errors(form.icon) }}
+
+        {% if client.icon_id %}
+          <img src="{{ client.icon.get_url() }}" class="client-icon">
+        {% endif %}
+      </div>
+
+      <hr>
+      <h3>OpenID/OAuth2 parameters</h3>
+
+      <div class="form-group">
+        <label class="form-label">OAuth2 Client ID</label>
+
+        <div class="input-group mt-2">
+          <input type="text" value="{{ client.oauth_client_id }}" class="form-control">
+          <span class="input-group-append">
+          <button
+              data-clipboard-text="{{ client.oauth_client_id }}"
+              class="clipboard btn btn-primary" type="button">
+            <i class="fe fe-clipboard"></i>
+          </button>
+        </span>
+        </div>
+      </div>
+
+      <div class="form-group">
+        <label class="form-label">OAuth2 Client Secret</label>
+
+        <div class="input-group mt-2">
+          <input type="password" value="{{ client.oauth_client_secret }}" class="form-control">
+          <span class="input-group-append">
+          <button
+              data-clipboard-text="{{ client.oauth_client_secret }}"
+              class="clipboard btn btn-primary" type="button">
+            <i class="fe fe-clipboard"></i>
+          </button>
+        </span>
+        </div>
+      </div>
+
+      <div class="form-group">
+        <label class="form-label">Authorized URIs</label>
+
+        {% for redirect_uri in client.redirect_uris %}
+          <div class="input-group mt-2">
+            <input type="url" name="uri" class="form-control" value="{{ redirect_uri.uri }}" required>
+
+            <span class="input-group-append">
+            <button class="remove-uri btn btn-primary" type="button">
+              <i class="fe fe-x"></i>
+            </button>
+          </span>
+
+          </div>
+        {% endfor %}
+
+        <div id="new-uris">
+          <!-- New uri will be put here -->
+        </div>
+
+
+        <button type="button" id="create-new-uri" class="mt-2 btn btn-outline-secondary">Add new uri</button>
+      </div>
+      <hr>
+      <button type="submit" class="btn btn-primary btn-lg">Update</button>
+    </form>
+
+    <!-- template for new uri -->
+    <div class="input-group mt-2" id="hidden-uri" style="display: none">
+      <input type="url" name="uri" class="form-control" required>
+
+      <span class="input-group-append">
+      <button class="remove-uri btn btn-primary" type="button">
+        <i class="fe fe-x"></i>
+      </button>
+    </span>
+
+    </div>
+
+  </div>
+
+{% endblock %}
+
+{% block script %}
+  <script type="text/x-template" id="course-detail">
+    <h1> ALO </h1>
+  </script>
+
+  <script>
+    require(["jquery", "notie", "clipboard"], function ($, notie, Clipboard) {
+
+      $("#create-new-uri").on("click", function (e) {
+        var clone = $("#hidden-uri").clone(true, true); // (true, true) to clone withDataAndEvents, deepWithDataAndEvents
+        clone.removeAttr("id");
+
+        $("#new-uris").append(clone);
+        clone.show();
+      });
+
+      $(".remove-uri").click(function (e) {
+        var currentElement = $(this);
+        currentElement.parent().parent().remove();
+      });
+
+      var clipboard = new Clipboard('.clipboard');
+
+      clipboard.on('success', function (e) {
+        notie.alert({
+          type: "success",
+          text: "Copied to clipboard",
+          time: 2,
+        });
+
+        e.clearSelection();
+      });
+    })
+  </script>
+{% endblock %}

+ 164 - 0
app/developer/templates/developer/index.html

@@ -0,0 +1,164 @@
+{% from "_formhelpers.html" import render_field %}
+
+{% extends 'default.html' %}
+
+{% set active_page = "developer" %}
+
+{% block title %}
+  Developer
+{% endblock %}
+
+{% block default_content %}
+  <div class="row">
+    <div class="col-4">
+      <a href="{{ url_for('developer.new_client') }}" class="btn btn-success">Create new app</a>
+    </div>
+  </div>
+
+  <div class="row row-cards row-deck mt-4">
+    <div class="col-12">
+      <div class="card">
+        <div class="table-responsive">
+          <table class="table table-hover table-outline table-vcenter text-nowrap card-table">
+            <thead>
+            <tr>
+              <th class="text-center w-1"><i class="icon-people"></i></th>
+              <th>Name</th>
+              <th>OAuth2 Client ID</th>
+              <th>Scopes</th>
+              <th>Number Users</th>
+              <th>Edit</th>
+              <!--<th>Publish</th>-->
+              <th>Delete</th>
+            </tr>
+            </thead>
+            <tbody>
+            {% for client in clients %}
+              <tr>
+                <td class="text-center">
+                  {% if client.icon_id %}
+                    <div class="avatar d-block" style="background-image: url({{ client.icon.get_url() }})">
+                      <span class="avatar-status bg-green"></span>
+                    </div>
+                  {% endif %}
+                </td>
+
+                <td>
+                  <div>
+                    <a href="{{ url_for('developer.client_detail', client_id=client.id) }}">
+                      {{ client.name }}
+                    </a>
+                  </div>
+                  <div class="small text-muted">
+                    Created at: {{ client.created_at |dt }}
+                  </div>
+                </td>
+
+                <td>
+                  {{ client.oauth_client_id }}
+                </td>
+
+                <td class="align-middle">
+                  <ul class="list-unstyled mb-0">
+                    {% for scope in client.scopes %}
+                      <li>
+                        <i class="fe fe-check"></i>
+                        {{ scope.name }}
+                      </li>
+                    {% endfor %}
+                  </ul>
+                </td>
+
+                <td>
+                  {{ client.nb_user() }}
+                </td>
+
+                <td>
+                  <a href="{{ url_for('developer.client_detail', client_id=client.id) }}" class="btn btn-info">
+                    <i class="fe fe-edit"></i>
+                  </a>
+                </td>
+
+              <!-- TODO: uncomment when bringing back "Discover" feature
+                <td>
+                  <form method="post">
+                    <input type="hidden" name="form-name" value="switch-client-publish">
+                    <input type="hidden" name="client-id" value="{{ client.id }}">
+
+                    <label class="custom-switch">
+                      <input type="checkbox" class="custom-switch-input"
+                          {{ "checked" if client.published else "" }}>
+                      <span class="custom-switch-indicator"></span>
+                    </label>
+                  </form>
+                </td>
+                -->
+
+                <td>
+                  <form method="post"
+                        onsubmit="return confirm('Please make sure no user is using this client. This operation is not reversible');">
+                    <input type="hidden" name="form-name" value="delete-client">
+                    <input type="hidden" name="client-id" value="{{ client.id }}">
+                    <button type="submit" class="btn btn-danger">
+                      <i class="fe fe-trash"></i>
+                    </button>
+                  </form>
+                </td>
+              </tr>
+            {% endfor %}
+
+            </tbody>
+          </table>
+        </div>
+      </div>
+    </div>
+  </div>
+
+{% endblock %}
+
+{% block script %}
+  <script>
+    require(['clipboard', 'notie', 'jquery'], function (Clipboard, notie, $) {
+      var clipboard = new Clipboard('.btn');
+
+      clipboard.on('success', function (e) {
+        notie.alert({
+          type: "success",
+          text: "Copied to clipboard",
+          time: 1,
+        });
+
+        e.clearSelection();
+      });
+
+      // the modal does not get close when user clicks outside of modal
+      // necessary for obligatory modal such as the one displayed when user enable/display email forwarding
+      notie.setOptions({
+        overlayClickDismiss: false,
+      });
+
+      $(".custom-switch-input").change(function (e) {
+        // Only ask for confirmation when publishing, not when un-publishing
+        if (e.target.checked) {
+          var message = `After this, your app/website will made available in "Discover", please confirm`;
+
+          notie.confirm({
+            text: message,
+            cancelCallback: () => {
+              // reset to the original value
+              var oldValue = !$(this).prop("checked");
+              $(this).prop("checked", oldValue);
+            },
+            submitCallback: () => {
+              $(this).closest("form").submit();
+            }
+          });
+        } else {
+          $(this).closest("form").submit();
+        }
+      })
+
+    });
+  </script>
+{% endblock %}
+

+ 37 - 0
app/developer/templates/developer/new_client.html

@@ -0,0 +1,37 @@
+{% from "_formhelpers.html" import render_field, render_field_errors %}
+
+{% extends 'default.html' %}
+
+{% set active_page = "developer" %}
+
+{% block title %}
+  Developer - Create new client
+{% endblock %}
+
+{% block default_content %}
+  <form method="post" enctype="multipart/form-data">
+    {{ form.csrf_token }}
+
+    <div class="form-group">
+      <label class="form-label">App Name</label>
+      {{ form.name(class="form-control") }}
+      {{ render_field_errors(form.name) }}
+    </div>
+
+    <div class="form-group">
+      <label class="form-label">Website Url</label>
+      {{ form.home_url(class="form-control", type="url") }}
+      {{ render_field_errors(form.home_url) }}
+    </div>
+
+    <div class="form-group">
+      <div class="form-label">App Icon</div>
+      {{ form.icon(class="form-control-file") }}
+      {{ render_field_errors(form.icon) }}
+    </div>
+
+    <button type="submit" class="btn btn-primary">Create</button>
+  </form>
+
+
+{% endblock %}

+ 0 - 0
app/developer/views/__init__.py


+ 71 - 0
app/developer/views/client_detail.py

@@ -0,0 +1,71 @@
+from io import BytesIO
+
+from flask import request, render_template, redirect, url_for, flash
+from flask_login import current_user, login_required
+from flask_wtf import FlaskForm
+from flask_wtf.file import FileField
+from wtforms import StringField, validators
+
+from app import s3
+from app.developer.base import developer_bp
+from app.extensions import db
+from app.log import LOG
+from app.models import Client, RedirectUri, File
+from app.utils import random_string
+
+
+class EditClientForm(FlaskForm):
+    name = StringField("Name", validators=[validators.DataRequired()])
+    icon = FileField("Icon")
+    home_url = StringField("Home Url")
+
+
+@developer_bp.route("/clients/<client_id>", methods=["GET", "POST"])
+@login_required
+def client_detail(client_id):
+    form = EditClientForm()
+
+    client = Client.get(client_id)
+    if not client:
+        flash("no such client", "warning")
+        return redirect(url_for("developer.index"))
+
+    if client.user_id != current_user.id:
+        flash("you cannot see this client", "warning")
+        return redirect(url_for("developer.index"))
+
+    if request.method == "POST":
+        if form.validate():
+            client.name = form.name.data
+            client.home_url = form.home_url.data
+
+            if form.icon.data:
+                # todo: remove current icon if any
+                # todo: handle remove icon
+                file_path = random_string(30)
+                file = File.create(path=file_path)
+
+                s3.upload_from_bytesio(file_path, BytesIO(form.icon.data.read()))
+
+                db.session.commit()
+                LOG.d("upload file %s to s3", file)
+
+                client.icon_id = file.id
+                db.session.commit()
+
+            uris = request.form.getlist("uri")
+
+            # replace all uris. TODO: optimize this?
+            for redirect_uri in client.redirect_uris:
+                redirect_uri.delete()
+
+            for uri in uris:
+                RedirectUri.create(client_id=client_id, uri=uri)
+
+            db.session.commit()
+
+            flash(f"client {client.name} has been updated", "success")
+
+            return redirect(url_for("developer.client_detail", client_id=client.id))
+
+    return render_template("developer/client_detail.html", form=form, client=client)

+ 52 - 0
app/developer/views/index.py

@@ -0,0 +1,52 @@
+"""List of clients"""
+from flask import render_template, request, flash, redirect, url_for
+from flask_login import current_user, login_required
+
+from app.developer.base import developer_bp
+from app.extensions import db
+from app.log import LOG
+from app.models import Client
+
+
+@developer_bp.route("/", methods=["GET", "POST"])
+@login_required
+def index():
+    # delete client
+    if request.method == "POST":
+        if request.form.get("form-name") == "delete-client":
+            client_id = int(request.form.get("client-id"))
+            client = Client.get(client_id)
+
+            if client.user_id != current_user.id:
+                flash("You cannot remove this client", "warning")
+            else:
+                client_name = client.name
+                client.delete()
+                db.session.commit()
+                LOG.d("Remove client %s", client)
+                flash(f"Client {client_name} has been deleted successfully", "success")
+
+        elif request.form.get("form-name") == "switch-client-publish":
+            client_id = int(request.form.get("client-id"))
+            client = Client.get(client_id)
+
+            if client.user_id != current_user.id:
+                flash("You cannot modify this client", "warning")
+            else:
+                client.published = not client.published
+                db.session.commit()
+                LOG.d("Switch client.published %s", client)
+
+                if client.published:
+                    flash(
+                        f"Client {client.name} has been published on Discover",
+                        "success",
+                    )
+                else:
+                    flash(f"Client {client.name} has been un-published", "success")
+
+        return redirect(url_for("developer.index"))
+
+    clients = Client.filter_by(user_id=current_user.id).all()
+
+    return render_template("developer/index.html", clients=clients)

+ 50 - 0
app/developer/views/new_client.py

@@ -0,0 +1,50 @@
+from io import BytesIO
+
+from flask import request, render_template, redirect, url_for, flash
+from flask_login import current_user, login_required
+from flask_wtf import FlaskForm
+from flask_wtf.file import FileField
+from wtforms import StringField, validators
+
+from app import s3
+from app.developer.base import developer_bp
+from app.extensions import db
+from app.log import LOG
+from app.models import Client, File
+from app.utils import random_string
+
+
+class NewClientForm(FlaskForm):
+    name = StringField("Name", validators=[validators.DataRequired()])
+    icon = FileField("Icon")
+    home_url = StringField("Home Url")
+
+
+@developer_bp.route("/new_client", methods=["GET", "POST"])
+@login_required
+def new_client():
+    form = NewClientForm()
+
+    if request.method == "POST":
+        if form.validate():
+            client = Client.create_new(form.name.data, current_user.id)
+            client.home_url = form.home_url.data
+            db.session.commit()
+
+            if form.icon.data:
+                file_path = random_string(30)
+                file = File.create(path=file_path)
+
+                s3.upload_from_bytesio(file_path, BytesIO(form.icon.data.read()))
+
+                db.session.commit()
+                LOG.d("upload file %s to s3", file)
+
+                client.icon_id = file.id
+                db.session.commit()
+
+            flash("New client has been created", "success")
+
+            return redirect(url_for("developer.client_detail", client_id=client.id))
+
+    return render_template("developer/new_client.html", form=form)

+ 1 - 0
app/discover/__init__.py

@@ -0,0 +1 @@
+from .views import index

+ 8 - 0
app/discover/base.py

@@ -0,0 +1,8 @@
+from flask import Blueprint
+
+discover_bp = Blueprint(
+    name="discover",
+    import_name=__name__,
+    url_prefix="/discover",
+    template_folder="templates",
+)

+ 41 - 0
app/discover/templates/discover/index.html

@@ -0,0 +1,41 @@
+{% extends 'default.html' %}
+
+{% set active_page = "discover" %}
+
+{% block title %}
+  Discover
+{% endblock %}
+
+{% block default_content %}
+
+  <h3>Apps</h3>
+  <p class="text-muted">
+    App/Website that have implemented <b>Connect with SimpeLogin</b>
+  </p>
+
+  <div class="row row-cards row-deck">
+    {% for client in clients %}
+      <div class="col-sm-4 col-xl-2">
+        <div class="card">
+          <a href="{{ client.home_url }}" target="_blank">
+            <img class="card-img-top" src="{{ client.get_icon_url() }}">
+          </a>
+          <div class="card-body d-flex flex-column">
+            <h4><a href="{{ client.home_url }}">
+              {{ client.name }}
+            </a></h4>
+
+            <div class="text-muted">
+              {{ client.home_url }}
+            </div>
+          </div>
+        </div>
+      </div>
+    {% endfor %}
+  </div>
+
+
+
+
+{% endblock %}
+

+ 0 - 0
app/discover/views/__init__.py


+ 12 - 0
app/discover/views/index.py

@@ -0,0 +1,12 @@
+from flask import render_template
+from flask_login import login_required
+
+from app.discover.base import discover_bp
+from app.models import Client
+
+
+@discover_bp.route("/", methods=["GET", "POST"])
+@login_required
+def index():
+    clients = Client.filter_by(published=True).all()
+    return render_template("discover/index.html", clients=clients)

+ 38 - 0
app/email_utils.py

@@ -0,0 +1,38 @@
+# using SendGrid's Python Library
+# https://github.com/sendgrid/sendgrid-python
+
+from sendgrid import SendGridAPIClient
+from sendgrid.helpers.mail import Mail
+
+from app.config import SUPPORT_EMAIL, SENDGRID_API_KEY, ENV
+from app.log import LOG
+
+
+def send(to_email, subject, html_content):
+    # On local only print out email content
+    if ENV == "local":
+        LOG.d(
+            "send mail to %s, subject:%s, content:%s", to_email, subject, html_content
+        )
+        return
+
+    message = Mail(
+        from_email=SUPPORT_EMAIL,
+        to_emails=to_email,
+        subject=subject,
+        html_content=html_content,
+    )
+    sg = SendGridAPIClient(SENDGRID_API_KEY)
+    response = sg.send(message)
+    LOG.d("sendgrid res:%s, email:%s", response.status_code, to_email)
+
+
+def notify_admin(subject, html_content):
+    send(
+        SUPPORT_EMAIL,
+        subject,
+        f"""
+        <html><body>
+    {html_content}
+    </body></html>""",
+    )

+ 4 - 30
app/extensions.py

@@ -1,34 +1,8 @@
 from flask_login import LoginManager
-from flask_sqlalchemy import SQLAlchemy, Model
+from flask_migrate import Migrate
+from flask_sqlalchemy import SQLAlchemy
 
 
-class CRUDMixin(Model):
-    """Mixin that adds convenience methods for CRUD (create, read, update, delete) operations."""
-
-    @classmethod
-    def create(cls, **kwargs):
-        """Create a new record and save it the database."""
-        instance = cls(**kwargs)
-        return instance.save()
-
-    def update(self, commit=True, **kwargs):
-        """Update specific fields of a record."""
-        for attr, value in kwargs.items():
-            setattr(self, attr, value)
-        return commit and self.save() or self
-
-    def save(self, commit=True):
-        """Save the record."""
-        db.session.add(self)
-        if commit:
-            db.session.commit()
-        return self
-
-    def delete(self, commit=True):
-        """Remove the record from the database."""
-        db.session.delete(self)
-        return commit and db.session.commit()
-
-
-db = SQLAlchemy(model_class=CRUDMixin)
+db = SQLAlchemy()
 login_manager = LoginManager()
+migrate = Migrate(db=db)

+ 47 - 0
app/jose_utils.py

@@ -0,0 +1,47 @@
+import arrow
+from jwcrypto import jwk, jwt
+
+from app.config import OPENID_PRIVATE_KEY_PATH, URL
+from app.log import LOG
+from app.models import ClientUser
+
+with open(OPENID_PRIVATE_KEY_PATH, "rb") as f:
+    key = jwk.JWK.from_pem(f.read())
+
+
+def get_jwk_key() -> dict:
+    return key._public_params()
+
+
+def make_id_token(client_user: ClientUser):
+    """Make id_token for OpenID Connect
+    According to RFC 7519, these claims are mandatory:
+    - iss
+    - sub
+    - aud
+    - exp
+    - iat
+    """
+    claims = {
+        "iss": URL,
+        "sub": str(client_user.id),
+        "aud": client_user.client.oauth_client_id,
+        "exp": arrow.now().shift(hours=1).timestamp,
+        "iat": arrow.now().timestamp,
+    }
+
+    claims = {**claims, **client_user.get_user_info()}
+
+    jwt_token = jwt.JWT(header={"alg": "RS256", "kid": "simple-login"}, claims=claims)
+    jwt_token.make_signed_token(key)
+    return jwt_token.serialize()
+
+
+def verify_id_token(id_token) -> bool:
+    try:
+        jwt.JWT(key=key, jwt=id_token)
+    except Exception:
+        LOG.exception("id token not verified")
+        return False
+    else:
+        return True

+ 50 - 14
app/log.py

@@ -2,22 +2,50 @@ import logging
 import sys
 import time
 
+import boto3
+import watchtower
+
+from app.config import (
+    AWS_ACCESS_KEY_ID,
+    AWS_SECRET_ACCESS_KEY,
+    AWS_REGION,
+    CLOUDWATCH_LOG_GROUP,
+    ENABLE_CLOUDWATCH,
+    CLOUDWATCH_LOG_STREAM,
+)
+
 _log_format = "%(asctime)s - %(name)s - %(levelname)s - %(process)d - %(module)s:%(lineno)d - %(funcName)s - %(message)s"
 _log_formatter = logging.Formatter(_log_format)
 
 
-def _get_console_handler(level=None):
+def _get_console_handler():
     console_handler = logging.StreamHandler(sys.stdout)
     console_handler.setFormatter(_log_formatter)
     console_handler.formatter.converter = time.gmtime
 
-    if level:
-        console_handler.setLevel(level)
-
     return console_handler
 
 
-def get_logger(name):
+def _get_watchtower_handler():
+    session = boto3.Session(
+        aws_access_key_id=AWS_ACCESS_KEY_ID,
+        aws_secret_access_key=AWS_SECRET_ACCESS_KEY,
+        region_name=AWS_REGION,
+    )
+
+    handler = watchtower.CloudWatchLogHandler(
+        log_group=CLOUDWATCH_LOG_GROUP,
+        stream_name=CLOUDWATCH_LOG_STREAM,
+        send_interval=5,  # every 5 sec
+        boto3_session=session,
+    )
+
+    handler.setFormatter(_log_formatter)
+
+    return handler
+
+
+def _get_logger(name):
     logger = logging.getLogger(name)
 
     logger.setLevel(logging.DEBUG)
@@ -25,7 +53,16 @@ def get_logger(name):
     # leave the handlers level at NOTSET so the level checking is only handled by the logger
     logger.addHandler(_get_console_handler())
 
-    # no propagation to avoid unexpected behaviour
+    if ENABLE_CLOUDWATCH:
+        print(
+            "enable cloudwatch, log group",
+            CLOUDWATCH_LOG_GROUP,
+            "; log stream:",
+            CLOUDWATCH_LOG_STREAM,
+        )
+        logger.addHandler(_get_watchtower_handler())
+
+    # no propagation to avoid propagating to root logger
     logger.propagate = False
 
     return logger
@@ -33,13 +70,12 @@ def get_logger(name):
 
 print(f">>> init logging <<<")
 
-# ### config root logger ###
-# do not use the default (buggy) logger
-logging.root.handlers.clear()
+# Disable flask logs such as 127.0.0.1 - - [15/Feb/2013 10:52:22] "GET /index.html HTTP/1.1" 200
+log = logging.getLogger("werkzeug")
+log.disabled = True
 
-# add handlers with the default level = "warn"
-# need to add level at handler level as there's no level check in root logger
-# all the libs logs having level >= WARN will be handled by these 2 handlers
-logging.root.addHandler(_get_console_handler(logging.WARN))
+# Set some shortcuts
+logging.Logger.d = logging.Logger.debug
+logging.Logger.i = logging.Logger.info
 
-LOG = get_logger("yourkey")
+LOG = _get_logger("sl")

+ 370 - 16
app/models.py

@@ -1,30 +1,141 @@
-# <<< Models >>>
-from datetime import datetime
+import enum
+import hashlib
 
+import arrow
 import bcrypt
+import stripe
+from arrow import Arrow
 from flask_login import UserMixin
+from sqlalchemy_utils import ArrowType
 
+from app import s3
+from app.config import URL, MAX_NB_EMAIL_FREE_PLAN, EMAIL_DOMAIN
 from app.extensions import db
+from app.log import LOG
+from app.oauth_models import ScopeE
+from app.utils import convert_to_id, random_string
 
 
 class ModelMixin(object):
     id = db.Column(db.Integer, primary_key=True, autoincrement=True)
-    created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
-    updated_at = db.Column(db.DateTime, default=None, onupdate=datetime.utcnow)
+    created_at = db.Column(ArrowType, default=arrow.utcnow, nullable=False)
+    updated_at = db.Column(ArrowType, default=None, onupdate=arrow.utcnow)
 
+    _repr_hide = ["created_at", "updated_at"]
+
+    @classmethod
+    def query(cls):
+        return db.session.query(cls)
+
+    @classmethod
+    def get(cls, id):
+        return cls.query.get(id)
+
+    @classmethod
+    def get_by(cls, **kw):
+        return cls.query.filter_by(**kw).first()
+
+    @classmethod
+    def filter_by(cls, **kw):
+        return cls.query.filter_by(**kw)
+
+    @classmethod
+    def get_or_create(cls, **kw):
+        r = cls.get_by(**kw)
+        if not r:
+            r = cls(**kw)
+            db.session.add(r)
+
+        return r
+
+    @classmethod
+    def create(cls, **kw):
+        r = cls(**kw)
+        db.session.add(r)
+        return r
+
+    def save(self):
+        db.session.add(self)
+
+    def delete(self):
+        db.session.delete(self)
+
+    def __repr__(self):
+        values = ", ".join(
+            "%s=%r" % (n, getattr(self, n))
+            for n in self.__table__.c.keys()
+            if n not in self._repr_hide
+        )
+        return "%s(%s)" % (self.__class__.__name__, values)
 
-class Client(db.Model, ModelMixin):
-    client_id = db.Column(db.String(128), unique=True)
-    client_secret = db.Column(db.String(128))
-    redirect_uri = db.Column(db.String(1024))
-    name = db.Column(db.String(128))
+
+class File(db.Model, ModelMixin):
+    path = db.Column(db.String(128), unique=True, nullable=False)
+
+    def get_url(self):
+        return s3.get_url(self.path)
+
+
+class PlanEnum(enum.Enum):
+    free = 0
+    trial = 1
+    monthly = 2
+    yearly = 3
 
 
 class User(db.Model, ModelMixin, UserMixin):
-    email = db.Column(db.String(128), unique=True)
+    __tablename__ = "users"
+    email = db.Column(db.String(128), unique=True, nullable=False)
     salt = db.Column(db.String(128), nullable=False)
     password = db.Column(db.String(128), nullable=False)
-    name = db.Column(db.String(128))
+    name = db.Column(db.String(128), nullable=False)
+    is_admin = db.Column(db.Boolean, nullable=False, default=False)
+
+    activated = db.Column(db.Boolean, default=False, nullable=False)
+
+    plan = db.Column(
+        db.Enum(PlanEnum),
+        nullable=False,
+        default=PlanEnum.free,
+        server_default=PlanEnum.free.name,
+    )
+
+    # only relevant for trial period
+    plan_expiration = db.Column(ArrowType)
+
+    stripe_customer_id = db.Column(db.String(128), unique=True)
+    stripe_card_token = db.Column(db.String(128), unique=True)
+    stripe_subscription_id = db.Column(db.String(128), unique=True)
+
+    profile_picture_id = db.Column(db.ForeignKey(File.id), nullable=True)
+    is_developer = db.Column(db.Boolean, nullable=False, server_default="0")
+
+    # contain the list of promo codes user has used. Promo codes are separated by ","
+    promo_codes = db.Column(db.Text, nullable=True)
+
+    profile_picture = db.relationship(File)
+
+    def should_upgrade(self):
+        """User is invited to upgrade if they are in free plan or their trial ends soon"""
+        if self.plan == PlanEnum.free:
+            return True
+        elif self.plan == PlanEnum.trial and self.plan_expiration < arrow.now().shift(
+            weeks=1
+        ):
+            return True
+        return False
+
+    def is_premium(self):
+        return self.plan in (PlanEnum.monthly, PlanEnum.yearly)
+
+    def can_create_new_email(self):
+        if self.is_premium():
+            return True
+        # plan not expired yet
+        elif self.plan == PlanEnum.trial and self.plan_expiration > arrow.now():
+            return True
+        else:  # free or trial expired
+            return GenEmail.filter_by(user_id=self.id).count() < MAX_NB_EMAIL_FREE_PLAN
 
     def set_password(self, password):
         salt = bcrypt.gensalt()
@@ -36,16 +147,259 @@ class User(db.Model, ModelMixin, UserMixin):
         password_hash = bcrypt.hashpw(password.encode(), self.salt.encode())
         return self.password.encode() == password_hash
 
+    def profile_picture_url(self):
+        if self.profile_picture_id:
+            return self.profile_picture.get_url()
+        else:  # use gravatar
+            hash_email = hashlib.md5(self.email.encode("utf-8")).hexdigest()
+            return f"https://www.gravatar.com/avatar/{hash_email}"
+
+    def plan_current_period_end(self) -> Arrow:
+        if not self.stripe_subscription_id:
+            LOG.error(
+                "plan_current_period_end should not be called with empty stripe_subscription_id"
+            )
+            return None
+
+        current_period_end_ts = stripe.Subscription.retrieve(
+            self.stripe_subscription_id
+        )["current_period_end"]
+
+        return arrow.get(current_period_end_ts)
+
+    def get_promo_codes(self) -> [str]:
+        if not self.promo_codes:
+            return []
+        return self.promo_codes.split(",")
+
+    def save_new_promo_code(self, promo_code):
+        current_promo_codes = self.get_promo_codes()
+        current_promo_codes.append(promo_code)
+
+        self.promo_codes = ",".join(current_promo_codes)
+
+
+class ActivationCode(db.Model, ModelMixin):
+    """For activate user account"""
+
+    user_id = db.Column(db.ForeignKey(User.id, ondelete="cascade"), nullable=False)
+    code = db.Column(db.String(128), unique=True, nullable=False)
+
+    user = db.relationship(User)
+
+    # the activation code is valid for 1h
+    expired = db.Column(ArrowType, default=arrow.now().shift(hours=1))
+
+
+class ResetPasswordCode(db.Model, ModelMixin):
+    """For resetting password"""
+
+    user_id = db.Column(db.ForeignKey(User.id, ondelete="cascade"), nullable=False)
+    code = db.Column(db.String(128), unique=True, nullable=False)
+
+    user = db.relationship(User)
+
+    # the activation code is valid for 1h
+    expired = db.Column(ArrowType, default=arrow.now().shift(hours=1), nullable=False)
+
+
+class Partner(db.Model, ModelMixin):
+    email = db.Column(db.String(128))
+    name = db.Column(db.String(128))
+    website = db.Column(db.String(1024))
+    additional_information = db.Column(db.Text)
+
+    # If apply from a authenticated user, set user_id to the user who has applied for partnership
+    user_id = db.Column(db.ForeignKey(User.id, ondelete="cascade"), nullable=True)
+
+
+# <<< OAUTH models >>>
+
+client_scope = db.Table(
+    "client_scope",
+    db.Column(
+        "client_id",
+        db.Integer,
+        db.ForeignKey("client.id", ondelete="cascade"),
+        primary_key=True,
+        nullable=False,
+    ),
+    db.Column(
+        "scope_id",
+        db.Integer,
+        db.ForeignKey("scope.id", ondelete="cascade"),
+        primary_key=True,
+        nullable=False,
+    ),
+)
+
+
+def generate_oauth_client_id(client_name) -> str:
+    oauth_client_id = convert_to_id(client_name) + "-" + random_string()
+
+    # check that the client does not exist yet
+    if not Client.get_by(oauth_client_id=oauth_client_id):
+        LOG.debug("generate oauth_client_id %s", oauth_client_id)
+        return oauth_client_id
+
+    # Rerun the function
+    LOG.warning(
+        "client_id %s already exists, generate a new client_id", oauth_client_id
+    )
+    return generate_oauth_client_id(client_name)
+
+
+class Client(db.Model, ModelMixin):
+    oauth_client_id = db.Column(db.String(128), unique=True, nullable=False)
+    oauth_client_secret = db.Column(db.String(128), nullable=False)
+
+    name = db.Column(db.String(128), nullable=False)
+    home_url = db.Column(db.String(1024))
+    published = db.Column(db.Boolean, default=False, nullable=False)
+
+    # user who created this client
+    user_id = db.Column(db.ForeignKey(User.id, ondelete="cascade"), nullable=False)
+    icon_id = db.Column(db.ForeignKey(File.id), nullable=True)
+
+    scopes = db.relationship("Scope", secondary=client_scope, lazy="subquery")
+    icon = db.relationship(File)
+
+    def nb_user(self):
+        return ClientUser.filter_by(client_id=self.id).count()
+
+    @classmethod
+    def create_new(cls, name, user_id) -> "Client":
+        # generate a client-id
+        oauth_client_id = generate_oauth_client_id(name)
+        oauth_client_secret = random_string(40)
+        client = Client.create(
+            name=name,
+            oauth_client_id=oauth_client_id,
+            oauth_client_secret=oauth_client_secret,
+            user_id=user_id,
+        )
+
+        # By default, add email and name scope
+        client.scopes.append(Scope.get_by(name=ScopeE.NAME.value))
+        client.scopes.append(Scope.get_by(name=ScopeE.EMAIL.value))
+
+        return client
+
+    def get_icon_url(self):
+        if self.icon_id:
+            return self.icon.get_url()
+        else:
+            return URL + "/static/default-icon.svg"
+
+
+class RedirectUri(db.Model, ModelMixin):
+    """Valid redirect uris for a client"""
+
+    client_id = db.Column(db.ForeignKey(Client.id, ondelete="cascade"), nullable=False)
+    uri = db.Column(db.String(1024), nullable=False)
+
+    client = db.relationship(Client, backref="redirect_uris")
+
 
 class AuthorizationCode(db.Model, ModelMixin):
-    code = db.Column(db.String(128), unique=True)
-    client_id = db.Column(db.ForeignKey(Client.id))
-    user_id = db.Column(db.ForeignKey(User.id))
+    code = db.Column(db.String(128), unique=True, nullable=False)
+    client_id = db.Column(db.ForeignKey(Client.id, ondelete="cascade"), nullable=False)
+    user_id = db.Column(db.ForeignKey(User.id, ondelete="cascade"), nullable=False)
+
+    scope = db.Column(db.String(128))
+    redirect_uri = db.Column(db.String(1024))
+
+    user = db.relationship(User, lazy=False)
+    client = db.relationship(Client, lazy=False)
 
 
 class OauthToken(db.Model, ModelMixin):
     access_token = db.Column(db.String(128), unique=True)
-    client_id = db.Column(db.ForeignKey(Client.id))
-    user_id = db.Column(db.ForeignKey(User.id))
+    client_id = db.Column(db.ForeignKey(Client.id, ondelete="cascade"), nullable=False)
+    user_id = db.Column(db.ForeignKey(User.id, ondelete="cascade"), nullable=False)
+
+    scope = db.Column(db.String(128))
+    redirect_uri = db.Column(db.String(1024))
 
     user = db.relationship(User)
+    client = db.relationship(Client)
+
+
+class Scope(db.Model, ModelMixin):
+    name = db.Column(db.String(128), unique=True, nullable=False)
+
+
+def generate_email() -> str:
+    """generate an email address that does not exist before"""
+    random_email = random_string(40) + "@" + EMAIL_DOMAIN
+
+    # check that the client does not exist yet
+    if not GenEmail.get_by(email=random_email):
+        LOG.debug("generate email %s", random_email)
+        return random_email
+
+    # Rerun the function
+    LOG.warning("email %s already exists, generate a new email", random_email)
+    return generate_email()
+
+
+class GenEmail(db.Model, ModelMixin):
+    """Generated email"""
+
+    user_id = db.Column(db.ForeignKey(User.id, ondelete="cascade"), nullable=False)
+    email = db.Column(db.String(128), unique=True, nullable=False)
+
+    enabled = db.Column(db.Boolean(), default=True, nullable=False)
+
+    @classmethod
+    def create_new_gen_email(cls, user_id):
+        random_email = generate_email()
+        return GenEmail.create(user_id=user_id, email=random_email)
+
+    def __repr__(self):
+        return f"<GenEmail {self.id} {self.email}>"
+
+
+class ClientUser(db.Model, ModelMixin):
+    __table_args__ = (
+        db.UniqueConstraint("user_id", "client_id", name="uq_client_user"),
+    )
+
+    user_id = db.Column(db.ForeignKey(User.id, ondelete="cascade"), nullable=False)
+    client_id = db.Column(db.ForeignKey(Client.id, ondelete="cascade"), nullable=False)
+
+    # Null means client has access to user original email
+    gen_email_id = db.Column(
+        db.ForeignKey(GenEmail.id, ondelete="cascade"), nullable=True
+    )
+
+    gen_email = db.relationship(GenEmail, backref="client_users")
+
+    user = db.relationship(User)
+    client = db.relationship(Client)
+
+    def get_email(self):
+        return self.gen_email.email if self.gen_email_id else self.user.email
+
+    def get_user_info(self) -> dict:
+        """return user info according to client scope
+        Return dict with key being scope name
+
+        """
+        res = {"id": self.id, "client": self.client.name, "email_verified": True}
+
+        for scope in self.client.scopes:
+            if scope.name == ScopeE.NAME.value:
+                res[ScopeE.NAME.value] = self.user.name
+            elif scope.name == ScopeE.EMAIL.value:
+                # Use generated email
+                if self.gen_email_id:
+                    LOG.debug(
+                        "Use gen email for user %s, client %s", self.user, self.client
+                    )
+                    res[ScopeE.EMAIL.value] = self.gen_email.email
+                # Use user original email
+                else:
+                    res[ScopeE.EMAIL.value] = self.user.email
+
+        return res

+ 1 - 4
app/monitor/views.py

@@ -1,9 +1,6 @@
-import subprocess
-
+from app.config import SHA1
 from app.monitor.base import monitor_bp
 
-SHA1 = subprocess.getoutput("git rev-parse HEAD")
-
 
 @monitor_bp.route("/git")
 def git_sha1():

+ 1 - 0
app/oauth/__init__.py

@@ -0,0 +1 @@
+from .views import authorize, token, user_info

+ 5 - 0
app/oauth/base.py

@@ -0,0 +1,5 @@
+from flask import Blueprint
+
+oauth_bp = Blueprint(
+    name="oauth", import_name=__name__, url_prefix="/oauth", template_folder="templates"
+)

+ 81 - 0
app/oauth/templates/oauth/authorize.html

@@ -0,0 +1,81 @@
+{% extends 'default.html' %}
+
+{% block title %}
+  Authorization
+{% endblock %}
+
+{% block default_content %}
+  <div class="col-md-6 offset-md-3">
+    <form class="card" method="post">
+      <div class="card-body p-6">
+        <!-- User has already authorized this client -->
+        {% if client_user %}
+          <div class="card-title">
+            You have already authorized <b>{{ client.name }}</b>.
+          </div>
+
+          <div>
+            <b>{{ client.name }}</b> has access to the following information:
+          </div>
+
+          <ul>
+            {% for scope in client.scopes %}
+              <li>{{ scope.name }}: {{ user_info[scope.name] }}</li>
+            {% endfor %}
+          </ul>
+        {% else %}
+          <div class="card-title">
+            <b>{{ client.name }}</b> will receive your following information:
+          </div>
+
+          <ul>
+            {% for scope in client.scopes %}
+              <li>{{ scope.name }}</li>
+            {% endfor %}
+          </ul>
+        {% endif %}
+
+        {% if client_user %}
+          <div class="form-footer">
+            <div class="btn-group" role="group" aria-label="Basic example">
+              <button type="submit" name="button" value="allow"
+                      class="btn btn-success">Allow
+              </button>
+
+              <a class="btn btn-light" href="javascript:history.back()">
+                Cancel
+              </a>
+            </div>
+          </div>
+        {% else %}
+          <div class="form-group">
+            <div class="custom-controls-stacked">
+              <label class="custom-control custom-checkbox">
+                <input type="checkbox" name="gen-email"
+                       class="custom-control-input" checked>
+                <span class="custom-control-label">Generate a new email</span>
+              </label>
+            </div>
+
+            <small class="form-text text-muted">
+              If checked, a new random email address will be generated for this app.
+            </small>
+          </div>
+
+          <div class="form-footer">
+            <div class="btn-group btn-block" role="group" aria-label="Basic example">
+              <button type="submit" name="button" value="allow"
+                      class="btn btn-success">Allow
+              </button>
+
+              <button type="submit" name="button" value="deny"
+                      class="btn btn-light">Deny
+              </button>
+            </div>
+          </div>
+        {% endif %}
+
+      </div>
+    </form>
+  </div>
+{% endblock %}

+ 39 - 0
app/oauth/templates/oauth/authorize_nonlogin_user.html

@@ -0,0 +1,39 @@
+{% extends "single.html" %}
+
+{% block single_content %}
+  <div class="row">
+    <b>{{ client.name }}</b> &nbsp; would like to have access to your following data:
+
+    <ul class="mt-3">
+      {% for scope in client.scopes %}
+        <li>{{ scope.name }}</li>
+      {% endfor %}
+    </ul>
+
+    <label>
+      In order to accept the request, you need to login or sign up.
+    </label>
+  </div>
+
+  <div class="row mt-4">
+    <div class="btn-group w-100">
+
+      <a href="{{ url_for('auth.login', next=next) }}" class="btn btn-success">
+        Login
+      </a>
+
+      <a href="{{ url_for('auth.register', next=next) }}" class="btn btn-info">
+        Sign Up
+      </a>
+    </div>
+  </div>
+
+  <hr>
+
+  <div class="row">
+    <a class="btn btn-block btn-secondary" href="javascript:history.back()">
+      <i class="fe fe-arrow-left mr-2"></i>Cancel
+    </a>
+  </div>
+
+{% endblock %}

+ 0 - 0
app/oauth/views/__init__.py


+ 197 - 0
app/oauth/views/authorize.py

@@ -0,0 +1,197 @@
+import random
+from typing import Dict
+from urllib.parse import urlparse
+
+from flask import request, render_template, redirect
+from flask_login import current_user
+
+from app.extensions import db
+from app.jose_utils import make_id_token
+from app.log import LOG
+from app.models import (
+    Client,
+    AuthorizationCode,
+    ClientUser,
+    GenEmail,
+    RedirectUri,
+    OauthToken,
+)
+from app.oauth.base import oauth_bp
+from app.oauth_models import get_response_types, ResponseType
+from app.utils import random_string, encode_url
+
+
+@oauth_bp.route("/authorize", methods=["GET", "POST"])
+def authorize():
+    """
+    Redirected from client when user clicks on "Login with Server".
+    This is a GET request with the following field in url
+    - client_id
+    - (optional) state
+    - response_type: must be code
+    """
+    oauth_client_id = request.args.get("client_id")
+    state = request.args.get("state")
+    scope = request.args.get("scope")
+    redirect_uri = request.args.get("redirect_uri")
+
+    try:
+        response_types: [ResponseType] = get_response_types(request)
+    except ValueError:
+        return (
+            "response_type must be code, token, id_token or certain combination of these."
+            " Please see /.well-known/openid-configuration to see what response_type are supported ",
+            400,
+        )
+
+    if not redirect_uri:
+        LOG.d("no redirect uri")
+        return "redirect_uri must be set", 400
+
+    client = Client.get_by(oauth_client_id=oauth_client_id)
+    if not client:
+        return f"no such client with oauth-client-id {oauth_client_id}", 400
+
+    # check if redirect_uri is valid
+    # allow localhost by default
+    # todo: only allow https
+    hostname, scheme = get_host_name_and_scheme(redirect_uri)
+    if hostname != "localhost":
+        if not RedirectUri.get_by(client_id=client.id, uri=redirect_uri):
+            return f"{redirect_uri} is not authorized", 400
+
+    # redirect from client website
+    if request.method == "GET":
+        if current_user.is_authenticated:
+            # user has already allowed this client
+            client_user: ClientUser = ClientUser.get_by(
+                client_id=client.id, user_id=current_user.id
+            )
+            user_info = {}
+            if client_user:
+                LOG.debug("user %s has already allowed client %s", current_user, client)
+                user_info = client_user.get_user_info()
+
+            return render_template(
+                "oauth/authorize.html", client=client, user_info=user_info
+            )
+        else:
+            # after user logs in, redirect user back to this page
+            return render_template(
+                "oauth/authorize_nonlogin_user.html", client=client, next=request.url
+            )
+    else:  # user allows or denies
+        gen_new_email = request.form.get("gen-email") == "on"
+
+        if request.form.get("button") == "deny":
+            LOG.debug("User %s denies Client %s", current_user, client)
+            final_redirect_uri = f"{redirect_uri}?error=deny&state={state}"
+            return redirect(final_redirect_uri)
+
+        LOG.debug("User %s allows Client %s", current_user, client)
+        client_user = ClientUser.get_by(client_id=client.id, user_id=current_user.id)
+
+        # user has already allowed this client
+        if client_user:
+            LOG.d("user %s has already allowed client %s", current_user, client)
+            # User cannot choose to gen new email
+            gen_new_email = False
+        else:
+            client_user = ClientUser.create(
+                client_id=client.id, user_id=current_user.id
+            )
+            db.session.flush()
+            LOG.d("create client-user for client %s, user %s", client, current_user)
+
+        redirect_args = {}
+
+        if state:
+            redirect_args["state"] = state
+        else:
+            LOG.warning(
+                "more security reason, state should be added. client %s", client
+            )
+
+        if scope:
+            redirect_args["scope"] = scope
+
+        for response_type in response_types:
+            if response_type == ResponseType.CODE:
+                # Create authorization code
+                auth_code = AuthorizationCode.create(
+                    client_id=client.id,
+                    user_id=current_user.id,
+                    code=random_string(),
+                    scope=scope,
+                    redirect_uri=redirect_uri,
+                )
+                db.session.add(auth_code)
+                redirect_args["code"] = auth_code.code
+            elif response_type == ResponseType.TOKEN:
+                # create access-token
+                oauth_token = OauthToken.create(
+                    client_id=client.id,
+                    user_id=current_user.id,
+                    scope=scope,
+                    redirect_uri=redirect_uri,
+                    access_token=generate_access_token(),
+                )
+                db.session.add(oauth_token)
+                redirect_args["access_token"] = oauth_token.access_token
+            elif response_type == ResponseType.ID_TOKEN:
+                redirect_args["id_token"] = make_id_token(client_user)
+
+        if gen_new_email:
+            client_user.gen_email_id = create_or_choose_gen_email(current_user).id
+
+        db.session.commit()
+
+        # construct redirect_uri with redirect_args
+        return redirect(construct_url(redirect_uri, redirect_args))
+
+
+def create_or_choose_gen_email(user) -> GenEmail:
+    can_create_new_email = user.can_create_new_email()
+
+    if can_create_new_email:
+        gen_email = GenEmail.create_new_gen_email(user_id=user.id)
+        db.session.flush()
+        LOG.debug("generate email %s for user %s", gen_email.email, user)
+    else:  # need to reuse one of the gen emails created
+        LOG.d("pick a random email for gen emails for user %s", current_user)
+        gen_emails = GenEmail.filter_by(user_id=current_user.id).all()
+        gen_email = random.choice(gen_emails)
+
+    return gen_email
+
+
+def construct_url(url, args: Dict[str, str]):
+    for i, (k, v) in enumerate(args.items()):
+        # make sure to escape v
+        v = encode_url(v)
+
+        if i == 0:
+            url += f"?{k}={v}"
+        else:
+            url += f"&{k}={v}"
+
+    return url
+
+
+def generate_access_token() -> str:
+    """generate an access-token that does not exist before"""
+    access_token = random_string(40)
+
+    if not OauthToken.get_by(access_token=access_token):
+        return access_token
+
+    # Rerun the function
+    LOG.warning("access token already exists, generate a new one")
+    return generate_access_token()
+
+
+def get_host_name_and_scheme(url: str) -> (str, str):
+    """http://localhost:5000?a=b -> (localhost, http) """
+    url_comp = urlparse(url)
+
+    return url_comp.hostname, url_comp.scheme

+ 88 - 0
app/oauth/views/token.py

@@ -0,0 +1,88 @@
+from flask import request, jsonify
+
+from app.extensions import db
+from app.jose_utils import make_id_token
+from app.log import LOG
+from app.models import Client, AuthorizationCode, OauthToken, ClientUser
+from app.oauth.base import oauth_bp
+from app.oauth.views.authorize import generate_access_token
+from app.oauth_models import ScopeE
+
+
+@oauth_bp.route("/token", methods=["POST"])
+def get_access_token():
+    """
+    Calls by client to exchange the access token given the authorization code.
+    The client authentications using Basic Authentication.
+    The form contains the following data:
+    - grant_type: must be "authorization_code"
+    - code: the code obtained in previous step
+    """
+    # Basic authentication
+    oauth_client_id = (
+        request.authorization and request.authorization.username
+    ) or request.form.get("client_id")
+
+    oauth_client_secret = (
+        request.authorization and request.authorization.password
+    ) or request.form.get("client_secret")
+
+    client = Client.filter_by(
+        oauth_client_id=oauth_client_id, oauth_client_secret=oauth_client_secret
+    ).first()
+
+    if not client:
+        return jsonify(error="wrong client-id or client-secret"), 400
+
+    # Get code from form data
+    grant_type = request.form.get("grant_type")
+    code = request.form.get("code")
+
+    # sanity check
+    if grant_type != "authorization_code":
+        return jsonify(error="grant_type must be authorization_code"), 400
+
+    auth_code: AuthorizationCode = AuthorizationCode.filter_by(code=code).first()
+    if not auth_code:
+        return jsonify(error=f"no such authorization code {code}"), 400
+
+    if auth_code.client_id != client.id:
+        return jsonify(error=f"are you sure this code belongs to you?"), 400
+
+    LOG.debug(
+        "Create Oauth token for user %s, client %s", auth_code.user, auth_code.client
+    )
+
+    # Create token
+    oauth_token = OauthToken.create(
+        client_id=auth_code.client_id,
+        user_id=auth_code.user_id,
+        scope=auth_code.scope,
+        redirect_uri=auth_code.redirect_uri,
+        access_token=generate_access_token(),
+    )
+    db.session.add(oauth_token)
+
+    # Auth code can be used only once
+    db.session.delete(auth_code)
+
+    db.session.commit()
+
+    client_user: ClientUser = ClientUser.get_by(
+        client_id=auth_code.client_id, user_id=auth_code.user_id
+    )
+
+    user_data = client_user.get_user_info()
+
+    res = {
+        "access_token": oauth_token.access_token,
+        "token_type": "bearer",
+        "expires_in": 3600,
+        "scope": "",
+        "user": user_data,
+    }
+
+    if oauth_token.scope and ScopeE.OPENID.value in oauth_token.scope:
+        res["id_token"] = make_id_token(client_user)
+
+    return jsonify(res)

+ 30 - 0
app/oauth/views/user_info.py

@@ -0,0 +1,30 @@
+from flask import request, jsonify
+from flask_cors import cross_origin
+
+from app.models import OauthToken, ClientUser
+from app.oauth.base import oauth_bp
+
+
+@oauth_bp.route("/user_info")
+@oauth_bp.route("/me")
+@oauth_bp.route("/userinfo")
+@cross_origin()
+def user_info():
+    """
+    Call by client to get user information
+    Usually bearer token is used.
+    """
+    if "AUTHORIZATION" in request.headers:
+        access_token = request.headers["AUTHORIZATION"].replace("Bearer ", "")
+    else:
+        access_token = request.args.get("access_token")
+
+    oauth_token: OauthToken = OauthToken.get_by(access_token=access_token)
+    if not oauth_token:
+        return jsonify(error="Invalid access token"), 400
+
+    client_user = ClientUser.get_or_create(
+        client_id=oauth_token.client_id, user_id=oauth_token.user_id
+    )
+
+    return jsonify(client_user.get_user_info())

+ 57 - 0
app/oauth_models.py

@@ -0,0 +1,57 @@
+import enum
+from typing import Set, Union
+
+import flask
+
+
+class ScopeE(enum.Enum):
+    """ScopeE to distinguish with Scope model"""
+
+    EMAIL = "email"
+    NAME = "name"
+    OPENID = "openid"
+
+
+class ResponseType(enum.Enum):
+    CODE = "code"
+    TOKEN = "token"
+    ID_TOKEN = "id_token"
+
+
+def get_scopes(request: flask.Request) -> Set[ScopeE]:
+    scope_strs = _split_arg(request.args.getlist("scope"))
+
+    return set([ScopeE(scope_str) for scope_str in scope_strs])
+
+
+def get_response_types(request: flask.Request) -> Set[ResponseType]:
+    response_type_strs = _split_arg(request.args.getlist("response_type"))
+
+    return set([ResponseType(r) for r in response_type_strs])
+
+
+def _split_arg(arg_input: Union[str, list]) -> Set[str]:
+    """convert input response_type/scope into a set of string.
+    arg_input = request.args.getlist(response_type|scope)
+    Take into account different variations and their combinations
+    - the split character is " " or ","
+    - the response_type/scope passed as a list ?scope=scope_1&scope=scope_2
+    """
+    res = set()
+    if type(arg_input) is str:
+        if " " in arg_input:
+            for x in arg_input.split(" "):
+                if x:
+                    res.add(x.lower())
+        elif "," in arg_input:
+            for x in arg_input.split(","):
+                if x:
+                    res.add(x.lower())
+        else:
+            res.add(arg_input)
+
+    else:
+        for arg in arg_input:
+            res = res.union(_split_arg(arg))
+
+    return res

+ 1 - 0
app/partner/__init__.py

@@ -0,0 +1 @@
+from .views import become

+ 8 - 0
app/partner/base.py

@@ -0,0 +1,8 @@
+from flask import Blueprint
+
+partner_bp = Blueprint(
+    name="partner",
+    import_name=__name__,
+    url_prefix="/partner",
+    template_folder="templates",
+)

+ 58 - 0
app/partner/templates/partner/become.html

@@ -0,0 +1,58 @@
+{% from "_formhelpers.html" import render_field, render_field_errors %}
+
+{% extends "single.html" %}
+
+{% block title %}
+  Become Partner
+{% endblock %}
+
+{% block single_content %}
+  {% if error %}
+    <div class="text-danger text-center mb-4">{{ error }}</div>
+  {% endif %}
+
+  <form class="card" method="post">
+    {{ form.csrf_token }}
+    <div class="card-body p-6">
+      <div class="card-title">Together, let's create the best login experience for users!</div>
+      <p class="text-muted">Becoming a partner will give you access to technical resources on SimpleLogin.</p>
+
+      <div class="form-group">
+        <label class="form-label">Your Email</label>
+        {{ form.email(class="form-control", type="email", placeholder="partner@my-app.com", value=current_user.email) }}
+        {{ render_field_errors(form.email) }}
+      </div>
+
+      <div class="form-group">
+        <label class="form-label">Your Business Name</label>
+        {{ form.name(class="form-control", placeholder="My App Inc", value=current_user.name) }}
+        {{ render_field_errors(form.name) }}
+      </div>
+
+      <div class="form-group">
+        <label class="form-label">Your Website/App URL</label>
+        {{ form.website(class="form-control", type="url", placeholder="https://my-app.com") }}
+        {{ render_field_errors(form.website) }}
+      </div>
+
+      <!-- Possibility to bypass using promo code. Only applied for user already authenticated -->
+      {% if current_user.is_authenticated %}
+        <hr>
+        <h4 class="text-center">
+          Or if you have a <em>partner code</em>, you can become a partner right away!
+        </h4>
+        <br>
+
+        <div class="form-group">
+          <label class="form-label">Partner Code</label>
+          {{ form.partner_code(class="form-control", type="text", placeholder="Partner Code") }}
+          {{ render_field_errors(form.partner_code) }}
+        </div>
+      {% endif %}
+
+      <div class="form-footer">
+        <button type="submit" class="btn btn-primary btn-block">Become a Partner</button>
+      </div>
+    </div>
+  </form>
+{% endblock %}

+ 0 - 0
app/partner/views/__init__.py


+ 77 - 0
app/partner/views/become.py

@@ -0,0 +1,77 @@
+from flask import request, render_template, redirect, url_for, flash
+from flask_login import current_user
+from flask_wtf import FlaskForm
+from wtforms import StringField
+
+from app.config import PARTNER_CODES
+from app.email_utils import notify_admin
+from app.extensions import db
+from app.models import Partner
+from app.partner.base import partner_bp
+
+
+class BecomePartnerForm(FlaskForm):
+    email = StringField("Email")
+    name = StringField("Name")
+    website = StringField("Website")
+    additional_information = StringField("Additional Information")
+    partner_code = StringField("Partner Code")
+
+
+@partner_bp.route("/become", methods=["GET", "POST"])
+def become():
+    form = BecomePartnerForm(request.form)
+
+    if form.validate_on_submit():
+        # bypass the application
+        if form.partner_code.data:
+            if not current_user.is_authenticated:
+                raise Exception("only authenticated user can enter partner code")
+
+            if form.partner_code.data in PARTNER_CODES:
+                notify_admin(
+                    f"User {current_user.name} has become partner!",
+                    {current_user.email},
+                )
+
+                current_user.is_developer = True
+                db.session.commit()
+
+                flash(
+                    "Congratulations, you are now a SimpleLogin partner! "
+                    "You will have access to tech resources on SimpleLogin.",
+                    "success",
+                )
+
+                return redirect(url_for("developer.index"))
+            else:
+                error = (
+                    "The partner code is unknown. Are you sure this is the right code?"
+                )
+                return render_template("partner/become.html", form=form, error=error)
+        else:
+            partner = Partner.create(
+                email=form.email.data,
+                name=form.name.data,
+                website=form.website.data,
+                additional_information=form.additional_information.data,
+            )
+
+            if current_user.is_authenticated:
+                partner.user_id = current_user.id
+
+            db.session.commit()
+
+            notify_admin(
+                f"New partner {partner.name} {partner.email} has signed up!",
+                partner.website,
+            )
+
+            flash(
+                "Your request has been submitted, we'll come back to you asap!",
+                "success",
+            )
+
+        return redirect(url_for("partner.become"))
+
+    return render_template("partner/become.html", form=form)

+ 38 - 0
app/s3.py

@@ -0,0 +1,38 @@
+from io import BytesIO
+
+import boto3
+
+from app.config import AWS_REGION, BUCKET, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
+
+session = boto3.Session(
+    aws_access_key_id=AWS_ACCESS_KEY_ID,
+    aws_secret_access_key=AWS_SECRET_ACCESS_KEY,
+    region_name=AWS_REGION,
+)
+
+
+def upload_from_bytesio(key: str, bs: BytesIO, content_type="string") -> None:
+    bs.seek(0)
+    session.resource("s3").Bucket(BUCKET).put_object(
+        Key=key, Body=bs, ContentType=content_type
+    )
+
+
+def delete_file(key: str) -> None:
+    o = session.resource("s3").Bucket(BUCKET).Object(key)
+    o.delete()
+
+
+def get_url(key: str) -> str:
+    """by default the link will expire in 1h (3600 seconds)"""
+    s3_client = session.client("s3")
+    return s3_client.generate_presigned_url(
+        ClientMethod="get_object", Params={"Bucket": BUCKET, "Key": key}
+    )
+
+
+if __name__ == "__main__":
+    with open("/tmp/1.png", "rb") as f:
+        upload_from_bytesio("1.png", BytesIO(f.read()))
+
+    print(get_url(BUCKET, "1.png"))

+ 15 - 0
app/utils.py

@@ -1,8 +1,23 @@
 import random
 import string
+import urllib.parse
+
+from unidecode import unidecode
 
 
 def random_string(length=10):
     """Generate a random string of fixed length """
     letters = string.ascii_lowercase
     return "".join(random.choice(letters) for _ in range(length))
+
+
+def convert_to_id(s: str):
+    """convert a string to id-like: remove space, remove special accent"""
+    s = s.replace(" ", "")
+    s = s.lower()
+    s = unidecode(s)
+    return s
+
+
+def encode_url(url):
+    return urllib.parse.quote(url, safe="")

+ 37 - 0
cron.py

@@ -0,0 +1,37 @@
+import arrow
+import stripe
+
+from app.extensions import db
+from app.log import LOG
+from app.models import User, PlanEnum
+from server import create_app
+
+
+def downgrade_expired_plan():
+    """set user plan to free when plan is expired, ie plan_expiration < now
+    """
+    for user in User.query.filter(
+        User.plan != PlanEnum.free, User.plan_expiration < arrow.now()
+    ).all():
+        LOG.d("set user %s to free plan", user)
+
+        user.plan_expiration = None
+        user.plan = PlanEnum.free
+
+        if user.stripe_customer_id:
+            LOG.d("delete user %s on stripe", user.stripe_customer_id)
+            stripe.Customer.delete(user.stripe_customer_id)
+
+            user.stripe_card_token = None
+            user.stripe_customer_id = None
+            user.stripe_subscription_id = None
+
+    db.session.commit()
+
+
+if __name__ == "__main__":
+    LOG.d("Start running cronjob")
+    app = create_app()
+
+    with app.app_context():
+        downgrade_expired_plan()

+ 6 - 0
crontab.yml

@@ -0,0 +1,6 @@
+jobs:
+  - name: downgrade_expired_plan
+    command: python /code/cron.py
+    shell: /bin/bash
+    schedule: "0 0 * * *"
+    captureStderr: true

+ 51 - 0
local_data/jwtRS256.key

@@ -0,0 +1,51 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIJKQIBAAKCAgEAveotF/UeMVHdm1FSgxflIbJr0yJZ1vyDGlQRK9DFx8HU8TVp
+9iqbY4CQEcOaa7cIVI5U0fWHW7kqByJ0BwLQciHienNZKnQishmMAkqNwfK3iJNc
+GFlNMhhrRGhEpWLox5qfpizK4xd7LK1tu2X5mEMWZtJs+wLr0SyVOPhdYCvOnSeT
+/SMSgDxvFCM1tlAv/wOV0SIF6xEIKb4lyHN3YKcs4z1IkqyPtHSeaq2BHeaFTPGq
+fAL2k4W7ziHxv7dsjCN9j11UnVQRKo+/kNJtOftH08l1J2FuG3YdTxX0R0KAl7wN
+QgvbGKjns1pj2az5uQKsne5SZBFjSe86Hbk8OKUJoJqy3LV29r7eZjj2wQoIBqbh
+BkMgPJY6rC9umWkaQKi79a24KeOEZPpTvbKy+LvoWh4UAs+7hfrKHRimQj2k74Jk
+SYxwrFejpxMYt+GJqPhympcz4gv4qIuiH2CV623/K9H+WrddIHpGCjpqkGtTZSQZ
+xyPMcEp+I26MuS1dFoaK39WFx2M62OhTenhDmOgPyWp1a71eYxzwfYtBECPJ4Agq
+SJrNIHu8/h9uZ+OTGXGN5k97BiWvqLEuz12PwH1QXX/sVzfjYi3khN0yxLYPooht
+fSQa8hg2VPJHVhVZNNCytC84E0xU5yNfIuUdIZjxJVcfV4C6dtt/QVQl9lsCAwEA
+AQKCAgAvuig2+xzpXB+LJvbLhzfILiS23M0jIDZ6aWIfVso9l1LCg5/rg22lpeuO
+609lfowTY+mhEklAHdqYDGqIUIa+CBH4oABqkOEfTRhIgx/4+9xv8EiWveqOimB6
+wpFt1tuVPiCdDGi4hXApHDSVgd0mDMYWdQ96TZOh78hYluIwhxHXoNiqJyRBIe7w
+aqDW/nPxbJ87/YbrOk6I3wZzx8Dag2jeespAQimjOiONv6jRMNuTKLCllcEN9e/q
+r9EnUxtuZITrgJMBLt1ZiuKjrJ5SkfnNGbXdfbjEIfzfoS7Qsb/LYjEaxgv7uIby
+JecuDzB69FcZIYmHKG+BZyN90M13J/bgpaMyYtdCZg+lJRO2gJvY34cw9oE9/M6O
+Lpfhx2viMQ2Mij1XNn9Qz0NIe89m8A0s1YbuDWieXU9iP4iA9YDPvsI0CwZoDT+W
+rLsSL3z7ltj8Ku6ySb655TFDPZysbMM2Oc8MmC9n7xuDfhuAr8eOqNgTtLLB0cnz
+aasASouAVtl5dN1hs5LakUq414wWhLzDqXd8kwRKFkT1WIBHy8+mk9MQj/m2rM+2
+avrIVKvdewRAB3TCwy0BdnWeiJER6r+Ae/Kglbo9NuDHJIkqwLZNbtX5xleJ+5Tp
+SoG/Lmz6AH+clL0IQYg6zLViI1tgPlYPt1ZZKp7bn+qDCn1/oQKCAQEA4eJwQ/gf
+3BtFvxmwpWKJnhKSACiJEfHimHIp/kAYtmtlhaSbEhYp3V699iqc6ziVHvAGssVi
+QCLGAuwdyaxAuWUYg+LSUic+hJagv5U+iJLYyYqI2PT1RjMnc/VoG+yqLpv7XyHd
+5/b64A2XNAsaX2DaUpdbTskxCZQ/l1ifRLR0mpxQQtZvXt4+2I1T3fvGcY6562G7
+dCSunm6fP5yvVKjg+j1ezCapF3aHJAV2OG6Mvu+shZxyACDQRmHpl9ujT64Ibcc6
+p1SmeHHr8/gOJY54gg/Iujne8GVGix7lTS1dqWXEF4xLlTomYD1FNZnt6bUqjqga
+9YZIvzID9FJ5cwKCAQEA1zwR1ajM27H4GvAGi+MfE2MTa99PEGGbJghAlMBzF8Bl
+He9SCADawOCejTiVBuWghU+qg3cb2JP/Qxnokd0eXXTiuHfJB3PwZPpfsiVUMKhN
+X1ypA06qvL2VLQNpCkgLuZB3pxkxn36EYM/NPqfZmQv25qsLC/eM5mRyWTu31kIw
+C4zRsHvy0IgHJJz9YJmcS/0PRnMvy96yXx/biYK80x3Zui7foCvRmPYeCCr/qoSb
+A9olFtv2yUPKt1m0lwxknl0tEhi7EiVNnOuWP416MhvJq0pz12CuYr5MHo3Zwmrw
+pyK0hlCmMePRQTe080oSDZP8UM/DkeMaFB+uw3N1eQKCAQBmoMsBFqrjBkEaIkHv
+4mVEPIu5JrGgRZX+TWBm9BhGSWVG4xLRlOBQg8srHRFOjdayx7tDXgrVuPbePQkL
+qAeAND5/LX8BdHMjKoy+fsB6rL1yVE74w9LsojE6rjUu+sgXhScggfKggcZaJdKd
+Aq5ox0hqXfpOQXrWL1T1Hn6+aH7SAFM3CtZu8+r52LxSDyKKVZ6DI1RX4JK1yOzx
+qe6/ODt/doKrnqUU0/VymEiuOwwXdC2eRwZEqKP4VmQbat84RInv1qT/gaZg8uGR
+ZxKGXcTC0wkQE1sHPfxfGRp1hjcXz/TX/hYZJuJot23KfLVribRcPGSDSQ+kTsUd
+LJuhAoIBAQCrjvfwREI2A59taVDug7SrcVdzrmWI+yP9pqpDZzrV/ccbmzzZoES9
+ZM08Z5NyEepnGF8jtvb9JMpco/QbABNKDvcAbopQZHuDIYbRqqt2tVAm6ObW+gdh
+tgOIA6XgShj+akbVbGF/bgr6V+iTPptVQJImvsNpYIJwyjPTKKSaJdvB+RbTA5lB
+2otHBdN5Ajfw4d8hGoNIj1PCOtR0wT7dUHfRzbb2JrdEozjA7fUn59bftSvHEsGd
+H2ofx2MI2xoAmOhp+khyaEV7BNWYBp8V/cw7unangCrADksCN7MRIsh7kFAwl2xB
+bAPJZivXmHzXUdPWXiTWzhxlWfOlWwyRAoIBAQCzAvoyoh/T6l9wrA7fbmiyHIJa
+82wKBkKXsbXqsxRuFYz4J9d5AmxE/QjIQpP8jfQwNDR6vB2Gzd8aQbb3edLV5gzM
+19X1brn5qluQOuzK+J76RKvrJKvC4YvYKwSFxujXQgELTQtPsqMuYiXEdAlS9V6/
+p8l5KlA9fEySPJGmQjfQkEvS082rMGQil2jjazuiRKxGabJ/kOpWeXURhw11MbbT
+AIfult3Mt6XxdGEWUm0ERHiuF3sr5QpYrwCPxOn0z4T4j4hJPMgbU+om8d1Oqp1k
+4+L6jF/eCYArqJOTS5oQ2SchKLrF5OYRNUDWLQtt3NiGxeJVfB++sp4losCx
+-----END RSA PRIVATE KEY-----

+ 14 - 0
local_data/jwtRS256.key.pub

@@ -0,0 +1,14 @@
+-----BEGIN PUBLIC KEY-----
+MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAveotF/UeMVHdm1FSgxfl
+IbJr0yJZ1vyDGlQRK9DFx8HU8TVp9iqbY4CQEcOaa7cIVI5U0fWHW7kqByJ0BwLQ
+ciHienNZKnQishmMAkqNwfK3iJNcGFlNMhhrRGhEpWLox5qfpizK4xd7LK1tu2X5
+mEMWZtJs+wLr0SyVOPhdYCvOnSeT/SMSgDxvFCM1tlAv/wOV0SIF6xEIKb4lyHN3
+YKcs4z1IkqyPtHSeaq2BHeaFTPGqfAL2k4W7ziHxv7dsjCN9j11UnVQRKo+/kNJt
+OftH08l1J2FuG3YdTxX0R0KAl7wNQgvbGKjns1pj2az5uQKsne5SZBFjSe86Hbk8
+OKUJoJqy3LV29r7eZjj2wQoIBqbhBkMgPJY6rC9umWkaQKi79a24KeOEZPpTvbKy
++LvoWh4UAs+7hfrKHRimQj2k74JkSYxwrFejpxMYt+GJqPhympcz4gv4qIuiH2CV
+623/K9H+WrddIHpGCjpqkGtTZSQZxyPMcEp+I26MuS1dFoaK39WFx2M62OhTenhD
+mOgPyWp1a71eYxzwfYtBECPJ4AgqSJrNIHu8/h9uZ+OTGXGN5k97BiWvqLEuz12P
+wH1QXX/sVzfjYi3khN0yxLYPoohtfSQa8hg2VPJHVhVZNNCytC84E0xU5yNfIuUd
+IZjxJVcfV4C6dtt/QVQl9lsCAwEAAQ==
+-----END PUBLIC KEY-----

+ 45 - 0
migrations/alembic.ini

@@ -0,0 +1,45 @@
+# A generic, single database configuration.
+
+[alembic]
+# template used to generate migration files
+# file_template = %%(rev)s_%%(slug)s
+
+# set to 'true' to run the environment during
+# the 'revision' command, regardless of autogenerate
+# revision_environment = false
+
+
+# Logging configuration
+[loggers]
+keys = root,sqlalchemy,alembic
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S

+ 96 - 0
migrations/env.py

@@ -0,0 +1,96 @@
+from __future__ import with_statement
+
+import logging
+from logging.config import fileConfig
+
+from sqlalchemy import engine_from_config
+from sqlalchemy import pool
+
+from alembic import context
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+
+# Interpret the config file for Python logging.
+# This line sets up loggers basically.
+fileConfig(config.config_file_name)
+logger = logging.getLogger('alembic.env')
+
+# add your model's MetaData object here
+# for 'autogenerate' support
+# from myapp import mymodel
+# target_metadata = mymodel.Base.metadata
+from flask import current_app
+config.set_main_option(
+    'sqlalchemy.url', current_app.config.get(
+        'SQLALCHEMY_DATABASE_URI').replace('%', '%%'))
+target_metadata = current_app.extensions['migrate'].db.metadata
+
+# other values from the config, defined by the needs of env.py,
+# can be acquired:
+# my_important_option = config.get_main_option("my_important_option")
+# ... etc.
+
+
+def run_migrations_offline():
+    """Run migrations in 'offline' mode.
+
+    This configures the context with just a URL
+    and not an Engine, though an Engine is acceptable
+    here as well.  By skipping the Engine creation
+    we don't even need a DBAPI to be available.
+
+    Calls to context.execute() here emit the given string to the
+    script output.
+
+    """
+    url = config.get_main_option("sqlalchemy.url")
+    context.configure(
+        url=url, target_metadata=target_metadata, literal_binds=True
+    )
+
+    with context.begin_transaction():
+        context.run_migrations()
+
+
+def run_migrations_online():
+    """Run migrations in 'online' mode.
+
+    In this scenario we need to create an Engine
+    and associate a connection with the context.
+
+    """
+
+    # this callback is used to prevent an auto-migration from being generated
+    # when there are no changes to the schema
+    # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
+    def process_revision_directives(context, revision, directives):
+        if getattr(config.cmd_opts, 'autogenerate', False):
+            script = directives[0]
+            if script.upgrade_ops.is_empty():
+                directives[:] = []
+                logger.info('No changes in schema detected.')
+
+    connectable = engine_from_config(
+        config.get_section(config.config_ini_section),
+        prefix='sqlalchemy.',
+        poolclass=pool.NullPool,
+    )
+
+    with connectable.connect() as connection:
+        context.configure(
+            connection=connection,
+            target_metadata=target_metadata,
+            process_revision_directives=process_revision_directives,
+            **current_app.extensions['migrate'].configure_args
+        )
+
+        with context.begin_transaction():
+            context.run_migrations()
+
+
+if context.is_offline_mode():
+    run_migrations_offline()
+else:
+    run_migrations_online()

+ 25 - 0
migrations/script.py.mako

@@ -0,0 +1,25 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+import sqlalchemy_utils
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade():
+    ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+    ${downgrades if downgrades else "pass"}

+ 29 - 0
migrations/versions/0256244cd7c8_.py

@@ -0,0 +1,29 @@
+"""empty message
+
+Revision ID: 0256244cd7c8
+Revises: 3cd10cfce8c3
+Create Date: 2019-06-28 11:19:50.401222
+
+"""
+import sqlalchemy_utils
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '0256244cd7c8'
+down_revision = '3cd10cfce8c3'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.add_column('activation_code', sa.Column('expired', sqlalchemy_utils.types.arrow.ArrowType(), nullable=True))
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_column('activation_code', 'expired')
+    # ### end Alembic commands ###

+ 24 - 0
migrations/versions/213fcca48483_.py

@@ -0,0 +1,24 @@
+"""empty message
+
+Revision ID: 213fcca48483
+Revises: 0256244cd7c8
+Create Date: 2019-06-30 11:11:51.823062
+
+"""
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects import postgresql
+
+# revision identifiers, used by Alembic.
+revision = '213fcca48483'
+down_revision = '0256244cd7c8'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    op.alter_column('users', 'trial_expiration', new_column_name='plan_expiration')
+
+
+def downgrade():
+    op.alter_column('users', 'plan_expiration', new_column_name='trial_expiration')

+ 28 - 0
migrations/versions/2fe19381f386_.py

@@ -0,0 +1,28 @@
+"""empty message
+
+Revision ID: 2fe19381f386
+Revises: d03e433dc248
+Create Date: 2019-07-01 11:47:24.934574
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '2fe19381f386'
+down_revision = 'd03e433dc248'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.add_column('users', sa.Column('is_developer', sa.Boolean(), server_default='0', nullable=False))
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_column('users', 'is_developer')
+    # ### end Alembic commands ###

+ 34 - 0
migrations/versions/3cd10cfce8c3_.py

@@ -0,0 +1,34 @@
+"""empty message
+
+Revision ID: 3cd10cfce8c3
+Revises: 5e549314e1e2
+Create Date: 2019-06-27 10:40:12.606337
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '3cd10cfce8c3'
+down_revision = '5e549314e1e2'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.add_column('authorization_code', sa.Column('redirect_uri', sa.String(length=1024), nullable=True))
+    op.add_column('authorization_code', sa.Column('scope', sa.String(length=128), nullable=True))
+    op.add_column('oauth_token', sa.Column('redirect_uri', sa.String(length=1024), nullable=True))
+    op.add_column('oauth_token', sa.Column('scope', sa.String(length=128), nullable=True))
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_column('oauth_token', 'scope')
+    op.drop_column('oauth_token', 'redirect_uri')
+    op.drop_column('authorization_code', 'scope')
+    op.drop_column('authorization_code', 'redirect_uri')
+    # ### end Alembic commands ###

+ 29 - 0
migrations/versions/590d89f981c0_.py

@@ -0,0 +1,29 @@
+"""empty message
+
+Revision ID: 590d89f981c0
+Revises: b20ee72fd9a4
+Create Date: 2019-07-01 21:46:58.613910
+
+"""
+import sqlalchemy_utils
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '590d89f981c0'
+down_revision = 'b20ee72fd9a4'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.add_column('users', sa.Column('promo_codes', sa.Text(), nullable=True))
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_column('users', 'promo_codes')
+    # ### end Alembic commands ###

+ 172 - 0
migrations/versions/5e549314e1e2_.py

@@ -0,0 +1,172 @@
+"""empty message
+
+Revision ID: 5e549314e1e2
+Revises: 
+Create Date: 2019-06-23 16:02:14.692075
+
+"""
+import sqlalchemy_utils
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+from sqlalchemy.dialects.postgresql import ENUM
+
+revision = '5e549314e1e2'
+down_revision = None
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # alembic cannot handle enum for now
+    enum = ENUM("free", "trial", "monthly", "yearly", name="plan_enum", create_type=False)
+    enum.create(op.get_bind(), checkfirst=False)
+
+
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.create_table('file',
+    sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
+    sa.Column('created_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=False),
+    sa.Column('updated_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=True),
+    sa.Column('path', sa.String(length=128), nullable=False),
+    sa.PrimaryKeyConstraint('id'),
+    sa.UniqueConstraint('path')
+    )
+    op.create_table('scope',
+    sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
+    sa.Column('created_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=False),
+    sa.Column('updated_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=True),
+    sa.Column('name', sa.String(length=128), nullable=False),
+    sa.PrimaryKeyConstraint('id'),
+    sa.UniqueConstraint('name')
+    )
+    op.create_table('users',
+    sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
+    sa.Column('created_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=False),
+    sa.Column('updated_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=True),
+    sa.Column('email', sa.String(length=128), nullable=False),
+    sa.Column('salt', sa.String(length=128), nullable=False),
+    sa.Column('password', sa.String(length=128), nullable=False),
+    sa.Column('name', sa.String(length=128), nullable=False),
+    sa.Column('is_admin', sa.Boolean(), nullable=False),
+    sa.Column('activated', sa.Boolean(), nullable=False),
+    sa.Column('plan', enum, server_default='free', nullable=False),
+    sa.Column('trial_expiration', sqlalchemy_utils.types.arrow.ArrowType(), nullable=True),
+    sa.Column('stripe_customer_id', sa.String(length=128), nullable=True),
+    sa.Column('stripe_card_token', sa.String(length=128), nullable=True),
+    sa.Column('stripe_subscription_id', sa.String(length=128), nullable=True),
+    sa.PrimaryKeyConstraint('id'),
+    sa.UniqueConstraint('email'),
+    sa.UniqueConstraint('stripe_card_token'),
+    sa.UniqueConstraint('stripe_customer_id'),
+    sa.UniqueConstraint('stripe_subscription_id')
+    )
+    op.create_table('activation_code',
+    sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
+    sa.Column('created_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=False),
+    sa.Column('updated_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=True),
+    sa.Column('user_id', sa.Integer(), nullable=False),
+    sa.Column('code', sa.String(length=128), nullable=False),
+    sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='cascade'),
+    sa.PrimaryKeyConstraint('id'),
+    sa.UniqueConstraint('code')
+    )
+    op.create_table('client',
+    sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
+    sa.Column('created_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=False),
+    sa.Column('updated_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=True),
+    sa.Column('oauth_client_id', sa.String(length=128), nullable=False),
+    sa.Column('oauth_client_secret', sa.String(length=128), nullable=False),
+    sa.Column('name', sa.String(length=128), nullable=False),
+    sa.Column('home_url', sa.String(length=1024), nullable=True),
+    sa.Column('published', sa.Boolean(), nullable=False),
+    sa.Column('user_id', sa.Integer(), nullable=False),
+    sa.Column('icon_id', sa.Integer(), nullable=True),
+    sa.ForeignKeyConstraint(['icon_id'], ['file.id'], ),
+    sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='cascade'),
+    sa.PrimaryKeyConstraint('id'),
+    sa.UniqueConstraint('oauth_client_id')
+    )
+    op.create_table('gen_email',
+    sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
+    sa.Column('created_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=False),
+    sa.Column('updated_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=True),
+    sa.Column('user_id', sa.Integer(), nullable=False),
+    sa.Column('email', sa.String(length=128), nullable=False),
+    sa.Column('enabled', sa.Boolean(), nullable=False),
+    sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='cascade'),
+    sa.PrimaryKeyConstraint('id'),
+    sa.UniqueConstraint('email')
+    )
+    op.create_table('authorization_code',
+    sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
+    sa.Column('created_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=False),
+    sa.Column('updated_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=True),
+    sa.Column('code', sa.String(length=128), nullable=False),
+    sa.Column('client_id', sa.Integer(), nullable=False),
+    sa.Column('user_id', sa.Integer(), nullable=False),
+    sa.ForeignKeyConstraint(['client_id'], ['client.id'], ondelete='cascade'),
+    sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='cascade'),
+    sa.PrimaryKeyConstraint('id'),
+    sa.UniqueConstraint('code')
+    )
+    op.create_table('client_scope',
+    sa.Column('client_id', sa.Integer(), nullable=False),
+    sa.Column('scope_id', sa.Integer(), nullable=False),
+    sa.ForeignKeyConstraint(['client_id'], ['client.id'], ondelete='cascade'),
+    sa.ForeignKeyConstraint(['scope_id'], ['scope.id'], ondelete='cascade'),
+    sa.PrimaryKeyConstraint('client_id', 'scope_id')
+    )
+    op.create_table('client_user',
+    sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
+    sa.Column('created_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=False),
+    sa.Column('updated_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=True),
+    sa.Column('user_id', sa.Integer(), nullable=False),
+    sa.Column('client_id', sa.Integer(), nullable=False),
+    sa.Column('gen_email_id', sa.Integer(), nullable=True),
+    sa.ForeignKeyConstraint(['client_id'], ['client.id'], ondelete='cascade'),
+    sa.ForeignKeyConstraint(['gen_email_id'], ['gen_email.id'], ondelete='cascade'),
+    sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='cascade'),
+    sa.PrimaryKeyConstraint('id'),
+    sa.UniqueConstraint('user_id', 'client_id', name='uq_client_user')
+    )
+    op.create_table('oauth_token',
+    sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
+    sa.Column('created_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=False),
+    sa.Column('updated_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=True),
+    sa.Column('access_token', sa.String(length=128), nullable=True),
+    sa.Column('client_id', sa.Integer(), nullable=False),
+    sa.Column('user_id', sa.Integer(), nullable=False),
+    sa.ForeignKeyConstraint(['client_id'], ['client.id'], ondelete='cascade'),
+    sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='cascade'),
+    sa.PrimaryKeyConstraint('id'),
+    sa.UniqueConstraint('access_token')
+    )
+    op.create_table('redirect_uri',
+    sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
+    sa.Column('created_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=False),
+    sa.Column('updated_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=True),
+    sa.Column('client_id', sa.Integer(), nullable=False),
+    sa.Column('uri', sa.String(length=1024), nullable=False),
+    sa.ForeignKeyConstraint(['client_id'], ['client.id'], ondelete='cascade'),
+    sa.PrimaryKeyConstraint('id')
+    )
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_table('redirect_uri')
+    op.drop_table('oauth_token')
+    op.drop_table('client_user')
+    op.drop_table('client_scope')
+    op.drop_table('authorization_code')
+    op.drop_table('gen_email')
+    op.drop_table('client')
+    op.drop_table('activation_code')
+    op.drop_table('users')
+    op.drop_table('scope')
+    op.drop_table('file')
+    # ### end Alembic commands ###

+ 40 - 0
migrations/versions/b20ee72fd9a4_.py

@@ -0,0 +1,40 @@
+"""empty message
+
+Revision ID: b20ee72fd9a4
+Revises: 2fe19381f386
+Create Date: 2019-07-01 13:15:05.391100
+
+"""
+import sqlalchemy_utils
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = 'b20ee72fd9a4'
+down_revision = '2fe19381f386'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.create_table('partner',
+    sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
+    sa.Column('created_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=False),
+    sa.Column('updated_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=True),
+    sa.Column('email', sa.String(length=128), nullable=True),
+    sa.Column('name', sa.String(length=128), nullable=True),
+    sa.Column('website', sa.String(length=1024), nullable=True),
+    sa.Column('additional_information', sa.Text(), nullable=True),
+    sa.Column('user_id', sa.Integer(), nullable=True),
+    sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='cascade'),
+    sa.PrimaryKeyConstraint('id')
+    )
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_table('partner')
+    # ### end Alembic commands ###

+ 39 - 0
migrations/versions/d03e433dc248_.py

@@ -0,0 +1,39 @@
+"""empty message
+
+Revision ID: d03e433dc248
+Revises: f234688f5ebd
+Create Date: 2019-06-30 23:24:28.486465
+
+"""
+import sqlalchemy_utils
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = 'd03e433dc248'
+down_revision = 'f234688f5ebd'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.create_table('reset_password_code',
+    sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
+    sa.Column('created_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=False),
+    sa.Column('updated_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=True),
+    sa.Column('user_id', sa.Integer(), nullable=False),
+    sa.Column('code', sa.String(length=128), nullable=False),
+    sa.Column('expired', sqlalchemy_utils.types.arrow.ArrowType(), nullable=False),
+    sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='cascade'),
+    sa.PrimaryKeyConstraint('id'),
+    sa.UniqueConstraint('code')
+    )
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_table('reset_password_code')
+    # ### end Alembic commands ###

+ 30 - 0
migrations/versions/f234688f5ebd_.py

@@ -0,0 +1,30 @@
+"""empty message
+
+Revision ID: f234688f5ebd
+Revises: 213fcca48483
+Create Date: 2019-06-30 18:30:55.295040
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = 'f234688f5ebd'
+down_revision = '213fcca48483'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.add_column('users', sa.Column('profile_picture_id', sa.Integer(), nullable=True))
+    op.create_foreign_key(None, 'users', 'file', ['profile_picture_id'], ['id'])
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_constraint(None, 'users', type_='foreignkey')
+    op.drop_column('users', 'profile_picture_id')
+    # ### end Alembic commands ###

+ 0 - 0
poc/__init__.py


+ 17 - 0
poc/jwt-jws-jwk.py

@@ -0,0 +1,17 @@
+"""
+ssh-keygen -t rsa -b 4096 -m PEM -f jwtRS256.key
+# Don't add passphrase
+openssl rsa -in jwtRS256.key -pubout -outform PEM -out jwtRS256.key.pub
+
+"""
+from jwcrypto import jwk, jws, jwt
+
+with open("jwtRS256.key", "rb") as f:
+    key = jwk.JWK.from_pem(f.read())
+
+Token = jwt.JWT(header={"alg": "RS256"}, claims={"info": "I'm a signed token"})
+Token.make_signed_token(key)
+print(Token.serialize())
+
+# verify
+jwt.JWT(key=key, jwt=Token.serialize())

+ 33 - 0
poc/poc_send_email.py

@@ -0,0 +1,33 @@
+"""POC on how to send email through postfix directly
+TODO: need to improve email score before using this
+"""
+
+import smtplib
+from email.mime.multipart import MIMEMultipart
+from email.mime.text import MIMEText
+
+fromaddr = "hello@u.sl.meo.ovh"
+toaddr = "test-7hxfo@mail-tester.com"
+
+# alternative is necessary so email client will display html version first, then use plain one as fall-back
+msg = MIMEMultipart("alternative")
+
+msg["From"] = fromaddr
+msg["To"] = toaddr
+msg["Subject"] = "test subject 2"
+
+msg.attach(MIMEText("test plain body", "plain"))
+msg.attach(
+    MIMEText(
+        """
+<html>
+<body>
+<b>Test body</b>
+</body>
+</html>""",
+        "html",
+    )
+)
+
+with smtplib.SMTP(host="localhost") as server:
+    server.sendmail(fromaddr, toaddr, msg.as_string())

+ 18 - 0
pyproject.toml

@@ -0,0 +1,18 @@
+[tool.black]
+exclude = '''
+(
+  /(
+      \.eggs         # exclude a few common directories in the
+    | \.git          # root of the project
+    | \.hg
+    | \.mypy_cache
+    | \.tox
+    | \.venv
+    | _build
+    | buck-out
+    | build
+    | dist
+    | migrations    # migrations/ is generated by alembic
+  )/
+)
+'''

+ 27 - 0
requirements.in

@@ -0,0 +1,27 @@
+flask_sqlalchemy
+flask
+flask_login
+wtforms
+unidecode
+gunicorn
+pip-tools
+bcrypt
+python-dotenv
+ipython
+sqlalchemy_utils
+psycopg2-binary
+sentry_sdk
+blinker
+arrow
+sendgrid
+Flask-WTF
+boto3
+Flask-Migrate
+flask_admin
+pytest
+flask-cors
+watchtower
+sqlalchemy-utils
+stripe
+jwcrypto
+yacron

+ 89 - 4
requirements.txt

@@ -1,4 +1,89 @@
-flask_sqlalchemy
-flask
-flask_login
-wtforms
+#
+# This file is autogenerated by pip-compile
+# To update, run:
+#
+#    pip-compile --output-file requirements.txt requirements.in
+#
+aiohttp==3.5.4            # via raven-aiohttp, yacron
+aiosmtplib==1.0.6         # via yacron
+alembic==1.0.10           # via flask-migrate
+appnope==0.1.0            # via ipython
+arrow==0.14.2
+asn1crypto==0.24.0        # via cryptography
+async-timeout==3.0.1      # via aiohttp
+atomicwrites==1.3.0       # via pytest
+attrs==19.1.0             # via aiohttp, pytest
+backcall==0.1.0           # via ipython
+bcrypt==3.1.6
+blinker==1.4
+boto3==1.9.167
+botocore==1.12.167        # via boto3, s3transfer
+certifi==2019.3.9         # via requests, sentry-sdk
+cffi==1.12.3              # via bcrypt, cryptography
+chardet==3.0.4            # via aiohttp, requests
+click==7.0                # via flask, pip-tools
+crontab==0.22.5           # via yacron
+cryptography==2.7         # via jwcrypto
+decorator==4.4.0          # via ipython, traitlets
+docutils==0.14            # via botocore
+flask-admin==1.5.3
+flask-cors==3.0.8
+flask-login==0.4.1
+flask-migrate==2.5.2
+flask-sqlalchemy==2.4.0
+flask-wtf==0.14.2
+flask==1.0.3
+gunicorn==19.9.0
+idna==2.8                 # via requests, yarl
+importlib-metadata==0.18  # via pluggy, pytest
+ipython-genutils==0.2.0   # via traitlets
+ipython==7.5.0
+itsdangerous==1.1.0       # via flask
+jedi==0.13.3              # via ipython
+jinja2==2.10.1            # via flask, yacron
+jmespath==0.9.4           # via boto3, botocore
+jwcrypto==0.6.0
+mako==1.0.12              # via alembic
+markupsafe==1.1.1         # via jinja2, mako
+more-itertools==7.0.0     # via pytest
+multidict==4.5.2          # via aiohttp, yarl
+packaging==19.0           # via pytest
+parso==0.4.0              # via jedi
+pexpect==4.7.0            # via ipython
+pickleshare==0.7.5        # via ipython
+pip-tools==3.8.0
+pluggy==0.12.0            # via pytest
+prompt-toolkit==2.0.9     # via ipython
+psycopg2-binary==2.8.2
+ptyprocess==0.6.0         # via pexpect
+py==1.8.0                 # via pytest
+pycparser==2.19           # via cffi
+pygments==2.4.2           # via ipython
+pyparsing==2.4.0          # via packaging
+pytest==4.6.3
+python-dateutil==2.8.0    # via alembic, arrow, botocore, strictyaml
+python-dotenv==0.10.3
+python-editor==1.0.4      # via alembic
+python-http-client==3.1.0  # via sendgrid
+raven-aiohttp==0.7.0      # via yacron
+raven==6.10.0             # via raven-aiohttp, yacron
+requests==2.22.0          # via stripe
+ruamel.yaml==0.15.97      # via strictyaml
+s3transfer==0.2.1         # via boto3
+sendgrid==6.0.5
+sentry-sdk==0.9.0
+six==1.12.0               # via bcrypt, cryptography, flask-cors, packaging, pip-tools, prompt-toolkit, pytest, python-dateutil, sqlalchemy-utils, traitlets
+sqlalchemy-utils==0.33.11
+sqlalchemy==1.3.4         # via alembic, flask-sqlalchemy, sqlalchemy-utils
+strictyaml==1.0.2         # via yacron
+stripe==2.30.1
+traitlets==4.3.2          # via ipython
+unidecode==1.0.23
+urllib3==1.25.3           # via botocore, requests, sentry-sdk
+watchtower==0.6.0
+wcwidth==0.1.7            # via prompt-toolkit, pytest
+werkzeug==0.15.4          # via flask
+wtforms==2.2.1
+yacron==0.9.0
+yarl==1.3.0               # via aiohttp
+zipp==0.5.1               # via importlib-metadata

+ 202 - 17
server.py

@@ -1,50 +1,112 @@
 import os
 
-from flask import Flask
+import arrow
+import sentry_sdk
+import stripe
+from flask import Flask, redirect, url_for, render_template, request, jsonify
+from flask_admin import Admin
+from flask_cors import cross_origin
+from flask_login import current_user
+from sentry_sdk.integrations.flask import FlaskIntegration
 
+from app.admin_model import SLModelView, SLAdminIndexView
 from app.auth.base import auth_bp
+from app.config import (
+    DB_URI,
+    FLASK_SECRET,
+    ENABLE_SENTRY,
+    ENV,
+    URL,
+    SHA1,
+    LYRA_ANALYTICS_ID,
+    STRIPE_SECRET_KEY,
+)
 from app.dashboard.base import dashboard_bp
-from app.extensions import db, login_manager
+from app.developer.base import developer_bp
+from app.discover.base import discover_bp
+from app.extensions import db, login_manager, migrate
+from app.jose_utils import get_jwk_key
 from app.log import LOG
-from app.models import Client, User
+from app.models import Client, User, Scope, ClientUser, GenEmail, RedirectUri, PlanEnum
 from app.monitor.base import monitor_bp
+from app.oauth.base import oauth_bp
+from app.oauth_models import ScopeE
+from app.partner.base import partner_bp
+
+if ENABLE_SENTRY:
+    LOG.d("enable sentry")
+    sentry_sdk.init(
+        dsn="https://ad2187ed843340a1b4165bd8d5d6cdce@sentry.io/1478143",
+        integrations=[FlaskIntegration()],
+    )
 
 
 def create_app() -> Flask:
     app = Flask(__name__)
+    app.url_map.strict_slashes = False
 
-    app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///db.sqlite"
+    app.config["SQLALCHEMY_DATABASE_URI"] = DB_URI
     app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
-    app.secret_key = "secret"
+    app.secret_key = FLASK_SECRET
 
     app.config["TEMPLATES_AUTO_RELOAD"] = True
 
     init_extensions(app)
     register_blueprints(app)
+    set_index_page(app)
+    jinja2_filter(app)
+
+    setup_error_page(app)
+
+    setup_favicon_route(app)
+    setup_openid_metadata(app)
+
+    stripe.api_key = STRIPE_SECRET_KEY
 
     return app
 
 
 def fake_data():
+    LOG.d("create fake data")
     # Remove db if exist
     if os.path.exists("db.sqlite"):
+        LOG.d("remove existing db file")
         os.remove("db.sqlite")
 
+    # Create all tables
     db.create_all()
 
     # fake data
-    client = Client(
-        client_id="client-id",
-        client_secret="client-secret",
-        redirect_uri="http://localhost:7000/callback",
-        name="Continental",
-    )
-    db.session.add(client)
+    Scope.create(name=ScopeE.NAME.value)
+    Scope.create(name=ScopeE.EMAIL.value)
+    db.session.commit()
 
-    user = User(id=1, email="john@wick.com", name="John Wick")
+    # Create a user
+    user = User.create(
+        email="nguyenkims+local@gmail.com",
+        name="Son Local",
+        activated=True,
+        is_admin=True,
+        is_developer=True,
+    )
     user.set_password("password")
-    db.session.add(user)
+    user.plan = PlanEnum.trial
+    user.plan_expiration = arrow.now().shift(weeks=2)
+    db.session.commit()
 
+    GenEmail.create_new_gen_email(user_id=user.id)
+
+    # Create a client
+    client1 = Client.create_new(name="Demo", user_id=user.id)
+    client1.home_url = "http://sl-client:7000"
+    client1.oauth_client_id = "client-id"
+    client1.oauth_client_secret = "client-secret"
+    client1.published = True
+    db.session.commit()
+
+    RedirectUri.create(client_id=client1.id, uri="http://sl-client:7000/callback")
+    RedirectUri.create(client_id=client1.id, uri="http://sl-client:7000/implicit")
+    RedirectUri.create(client_id=client1.id, uri="http://sl-client:7000/implicit-jso")
     db.session.commit()
 
 
@@ -59,18 +121,141 @@ def register_blueprints(app: Flask):
     app.register_blueprint(auth_bp)
     app.register_blueprint(monitor_bp)
     app.register_blueprint(dashboard_bp)
+    app.register_blueprint(developer_bp)
+    app.register_blueprint(partner_bp)
+
+    app.register_blueprint(oauth_bp, url_prefix="/oauth")
+    app.register_blueprint(oauth_bp, url_prefix="/oauth2")
+
+    app.register_blueprint(discover_bp)
+
+
+def set_index_page(app):
+    @app.route("/")
+    def index():
+        if current_user.is_authenticated:
+            return redirect(url_for("dashboard.index"))
+        else:
+            return redirect(url_for("auth.login"))
+
+    @app.after_request
+    def after_request(res):
+        # not logging /static call
+        if not request.path.startswith("/static") and not request.path.startswith(
+            "/admin/static"
+        ):
+            LOG.debug(
+                "%s %s %s %s %s",
+                request.remote_addr,
+                request.method,
+                request.path,
+                request.args,
+                res.status_code,
+            )
+        return res
+
+
+def setup_openid_metadata(app):
+    @app.route("/.well-known/openid-configuration")
+    @cross_origin()
+    def openid_config():
+        res = {
+            "issuer": URL,
+            "authorization_endpoint": URL + "/oauth2/authorize",
+            "token_endpoint": URL + "/oauth2/token",
+            "jwks_uri": URL + "/jwks",
+            "response_types_supported": [
+                "code",
+                "token",
+                "id_token",
+                "id_token token",
+                "id_token code",
+            ],
+            "subject_types_supported": ["public"],
+            "id_token_signing_alg_values_supported": ["RS256"],
+            # todo: add introspection and revocation endpoints
+            "introspection_endpoint": URL + "/oauth2/token/introspection",
+            "revocation_endpoint": URL + "/oauth2/token/revocation",
+        }
+
+        return jsonify(res)
+
+    @app.route("/jwks")
+    @cross_origin()
+    def jwks():
+        res = {"keys": [get_jwk_key()]}
+        return jsonify(res)
+
+
+def setup_error_page(app):
+    @app.errorhandler(400)
+    def page_not_found(e):
+        return render_template("error/400.html"), 400
+
+    @app.errorhandler(401)
+    def page_not_found(e):
+        return render_template("error/401.html", current_url=request.full_path), 401
+
+    @app.errorhandler(403)
+    def page_not_found(e):
+        return render_template("error/403.html"), 403
+
+    @app.errorhandler(404)
+    def page_not_found(e):
+        return render_template("error/404.html"), 404
+
+    @app.errorhandler(Exception)
+    def error_handler(e):
+        LOG.exception(e)
+        return render_template("error/500.html"), 500
+
+
+def setup_favicon_route(app):
+    @app.route("/favicon.ico")
+    def favicon():
+        return redirect("/static/favicon.ico")
+
+
+def jinja2_filter(app):
+    def format_datetime(value):
+        dt = arrow.get(value)
+        return dt.humanize()
+
+    app.jinja_env.filters["dt"] = format_datetime
+
+    @app.context_processor
+    def inject_stage_and_region():
+        return dict(
+            YEAR=arrow.now().year,
+            URL=URL,
+            ENABLE_SENTRY=ENABLE_SENTRY,
+            VERSION=SHA1,
+            LYRA_ANALYTICS_ID=LYRA_ANALYTICS_ID,
+        )
 
 
 def init_extensions(app: Flask):
     LOG.debug("init extensions")
     login_manager.init_app(app)
     db.init_app(app)
+    migrate.init_app(app)
+
+
+def init_admin(app):
+    admin = Admin(name="SimpleLogin", template_mode="bootstrap3")
+    admin.init_app(app, index_view=SLAdminIndexView())
+    admin.add_view(SLModelView(User, db.session))
+    admin.add_view(SLModelView(Client, db.session))
+    admin.add_view(SLModelView(GenEmail, db.session))
+    admin.add_view(SLModelView(ClientUser, db.session))
 
 
 if __name__ == "__main__":
     app = create_app()
 
-    with app.app_context():
-        fake_data()
+    if ENV == "local":
+        LOG.d("reset db, add fake data")
+        with app.app_context():
+            fake_data()
 
-    app.run(debug=True, threaded=False)
+    app.run(debug=True, host="0.0.0.0")

+ 65 - 0
shell.py

@@ -0,0 +1,65 @@
+import flask_migrate
+import stripe
+from IPython import embed
+from sqlalchemy_utils import create_database, database_exists, drop_database
+
+from app.config import DB_URI
+from app.models import *
+from app.oauth_models import ScopeE
+from server import create_app
+
+
+def create_db():
+    if not database_exists(DB_URI):
+        LOG.debug("db not exist, create database")
+        create_database(DB_URI)
+
+        # Create all tables
+        # Use flask-migrate instead of db.create_all()
+        flask_migrate.upgrade()
+
+        scope_name = Scope.create(name=ScopeE.NAME.value)
+        db.session.add(scope_name)
+        scope_email = Scope.create(name=ScopeE.EMAIL.value)
+        db.session.add(scope_email)
+        db.session.commit()
+
+
+def add_real_data():
+    """after the db is reset, add some accounts
+    TODO: remove this after adding alembic"""
+    user = User.create(email="nguyenkims@gmail.com", name="Son GM", activated=True)
+    user.set_password("password")
+    db.session.commit()
+
+    # Create a client
+    client1 = Client.create_new(name="Demo", user_id=user.id)
+    client1.oauth_client_id = "client-id"
+    client1.oauth_client_secret = "client-secret"
+    db.session.commit()
+
+    RedirectUri.create(client_id=client1.id, uri="http://demo.sl.meo.ovh/callback")
+    db.session.commit()
+
+    user2 = User.create(email="nguyenkims@hotmail.com", name="Son HM", activated=True)
+    user2.set_password("password")
+    db.session.commit()
+
+
+def change_password(user_id, new_password):
+    user = User.get(user_id)
+    user.set_password(new_password)
+    db.session.commit()
+
+
+def reset_db():
+    if database_exists(DB_URI):
+        drop_database(DB_URI)
+    create_db()
+    add_real_data()
+
+
+app = create_app()
+
+with app.app_context():
+    embed()

File diff suppressed because it is too large
+ 16745 - 0
static/assets/css/dashboard.css


File diff suppressed because it is too large
+ 16745 - 0
static/assets/css/dashboard.rtl.css


BIN
static/assets/fonts/feather/feather-webfont.eot


+ 1038 - 0
static/assets/fonts/feather/feather-webfont.svg

@@ -0,0 +1,1038 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
+<metadata>
+Created by FontForge 20170910 at Tue Jan 16 19:54:31 2018
+ By jimmywarting
+</metadata>
+<defs>
+<font id="feather" horiz-adv-x="1024" >
+  <font-face 
+    font-family="feather"
+    font-weight="400"
+    font-stretch="normal"
+    units-per-em="1024"
+    panose-1="0 0 0 0 0 0 0 0 0 0"
+    ascent="960"
+    descent="-64"
+    bbox="-7.53125 -90 1033 940.2"
+    underline-thickness="0"
+    underline-position="0"
+    unicode-range="U+0001-FFFD"
+  />
+<missing-glyph 
+ />
+    <glyph glyph-name=".notdef" unicode="&#xfffd;" 
+ />
+    <glyph glyph-name="glyph1" horiz-adv-x="0" 
+d="M0 0v0v0v0z" />
+    <glyph glyph-name="glyph1" horiz-adv-x="0" 
+d="M0 0v0v0v0z" />
+    <glyph glyph-name="uni0001" horiz-adv-x="0" 
+d="M0 0v0v0v0z" />
+    <glyph glyph-name="space" unicode=" " horiz-adv-x="512" 
+d="M0 0v0v0v0z" />
+    <glyph glyph-name="uniE900" unicode="&#xe900;" 
+d="M939 469h-171q-13 0 -24 -8.5t-14 -21.5l-90 -260l-218 644q-3 13 -14 21.5t-24 8.5t-24 -8.5t-14 -21.5l-120 -354h-141q-19 0 -30.5 -11.5t-11.5 -30.5q0 -20 11.5 -31.5t30.5 -11.5h171q13 0 24 8.5t14 21.5l90 260l218 -648q3 -13 14 -21.5t24 -8.5t24 8.5t14 21.5
+l120 354h141q19 0 30.5 11.5t11.5 30.5q0 20 -11.5 33.5t-30.5 13.5v0z" />
+    <glyph glyph-name="uniE901" unicode="&#xe901;" 
+d="M853 853h-682q-55 0 -91.5 -36.5t-36.5 -91.5v-426q0 -55 36.5 -91.5t91.5 -36.5h42q20 0 31.5 11.5t11.5 30.5q0 20 -11.5 31.5t-31.5 11.5h-42q-20 0 -31.5 11.5t-11.5 31.5v426q0 20 11.5 31.5t31.5 11.5h682q20 0 31.5 -11.5t11.5 -31.5v-426q0 -20 -11.5 -31.5
+t-31.5 -11.5h-42q-20 0 -31.5 -11.5t-11.5 -31.5q0 -19 11.5 -30.5t31.5 -11.5h42q55 0 91.5 36.5t36.5 91.5v426q0 55 -36.5 91.5t-91.5 36.5zM546 324q-13 16 -33.5 16t-30.5 -16l-213 -256q-7 -9 -9 -21.5t5 -25.5q3 -9 12 -15t22 -6h426q13 0 22.5 6.5t16.5 19.5
+q6 12 4 24.5t-9 22.5l-213 251v0zM388 85l124 145l124 -145h-248v0z" />
+    <glyph glyph-name="uniE902" unicode="&#xe902;" 
+d="M512 896q-98 0 -183 -37q-86 -36 -149.5 -100t-100.5 -149q-36 -86 -36 -183q0 -98 36 -183q37 -86 100.5 -149.5t149.5 -100.5q85 -37 183 -37t183 37q86 37 149.5 100.5t100.5 149.5q36 85 36 183q0 97 -36 183q-37 85 -100.5 149t-149.5 100q-85 37 -183 37zM512 43
+q-80 0 -150 30t-122 82t-82 122t-30 150t30 150t82 122t122 82t150 30t150 -30t122 -82t82 -122t30 -150t-30 -150t-82 -122t-122 -82t-150 -30zM512 640q-19 0 -31 -11.5t-12 -31.5v-170q0 -20 12 -31.5t31 -11.5t31 11.5t12 31.5v170q0 20 -12 31.5t-31 11.5v0zM482 286
+q-6 -7 -9.5 -13.5t-3.5 -16.5t3.5 -16.5t9.5 -13.5q7 -6 13.5 -9.5t16.5 -3.5t16.5 3.5t13.5 9.5q6 7 9.5 15.5t3.5 14.5t-3.5 14.5t-9.5 15.5q-13 13 -30 13t-30 -13v0z" />
+    <glyph glyph-name="uniE903" unicode="&#xe903;" 
+d="M969 631l-252 252q-3 7 -11.5 10t-18.5 3h-350q-10 0 -18.5 -3t-11.5 -10l-252 -252q-6 -3 -9 -11.5t-3 -17.5v-355q0 -9 3 -16t9 -13l252 -252q3 -3 11.5 -6t18.5 -3h354q10 0 17 3.5t13 9.5l252 252q6 6 9.5 13t3.5 17v350q-4 9 -7.5 17.5t-9.5 11.5v0zM896 269
+l-226 -226h-316l-226 226v316l226 226h320l222 -226v-316v0zM512 640q-19 0 -31 -11.5t-12 -31.5v-170q0 -20 12 -31.5t31 -11.5t31 11.5t12 31.5v170q0 20 -12 31.5t-31 11.5v0zM482 286q-6 -7 -9.5 -13.5t-3.5 -16.5t3.5 -16.5t9.5 -13.5q7 -6 13.5 -9.5t16.5 -3.5
+t16.5 3.5t13.5 9.5q6 7 9.5 15.5t3.5 14.5t-3.5 14.5t-9.5 15.5q-13 13 -30 13t-30 -13v0z" />
+    <glyph glyph-name="uniE904" unicode="&#xe904;" 
+d="M981 192l-358 606q-13 22 -34.5 37.5t-46.5 22.5q-26 6 -51 3t-47 -16q-13 -7 -25 -18.5t-18 -24.5v0v0l-358 -610q-26 -45 -12 -97t59 -78q12 -10 27.5 -13.5t31.5 -3.5h726q25 0 49 9.5t40 28.5q19 20 29 42t10 48q-4 16 -8 33.5t-14 30.5v0zM905 98q-7 -6 -15.5 -9.5
+t-14.5 -3.5h-726q-6 0 -10.5 0.5t-10.5 4.5q-16 9 -19.5 26t2.5 33l363 602q3 3 6 8t6 5q16 9 33.5 4.5t26.5 -17.5l363 -602q3 -3 3.5 -9t0.5 -12q3 -10 -1 -16.5t-7 -13.5v0zM512 597q-19 0 -31 -11.5t-12 -30.5v-171q0 -19 12 -31t31 -12t31 12t12 31v171q0 19 -12 30.5
+t-31 11.5zM482 243q-6 -6 -9.5 -13t-3.5 -17q0 -9 3.5 -16t9.5 -14q7 -6 13.5 -9t16.5 -3t16.5 3t13.5 9q6 7 9.5 15.5t3.5 14.5q0 7 -3.5 15.5t-9.5 14.5q-13 13 -30 13t-30 -13v0z" />
+    <glyph glyph-name="uniE905" unicode="&#xe905;" 
+d="M256 555q-19 0 -31 -12t-12 -31t12 -31t31 -12h512q19 0 31 12t12 31t-12 31t-31 12h-512zM128 640h768q19 0 31 11.5t12 31.5q0 19 -12 30.5t-31 11.5h-768q-19 0 -31 -11.5t-12 -30.5q0 -20 12 -31.5t31 -11.5zM896 384h-768q-19 0 -31 -11.5t-12 -31.5q0 -19 12 -30.5
+t31 -11.5h768q19 0 31 11.5t12 30.5q0 20 -12 31.5t-31 11.5zM768 213h-512q-19 0 -31 -11.5t-12 -30.5q0 -20 12 -31.5t31 -11.5h512q19 0 31 11.5t12 31.5q0 19 -12 30.5t-31 11.5z" />
+    <glyph glyph-name="uniE906" unicode="&#xe906;" 
+d="M896 555h-768q-19 0 -31 -12t-12 -31t12 -31t31 -12h768q19 0 31 12t12 31t-12 31t-31 12zM128 640h768q19 0 31 11.5t12 31.5q0 19 -12 30.5t-31 11.5h-768q-19 0 -31 -11.5t-12 -30.5q0 -20 12 -31.5t31 -11.5zM896 384h-768q-19 0 -31 -11.5t-12 -31.5q0 -19 12 -30.5
+t31 -11.5h768q19 0 31 11.5t12 30.5q0 20 -12 31.5t-31 11.5zM896 213h-768q-19 0 -31 -11.5t-12 -30.5q0 -20 12 -31.5t31 -11.5h768q19 0 31 11.5t12 31.5q0 19 -12 30.5t-31 11.5z" />
+    <glyph glyph-name="uniE907" unicode="&#xe907;" 
+d="M128 469h597q20 0 31.5 12t11.5 31t-11.5 31t-31.5 12h-597q-19 0 -31 -12t-12 -31t12 -31t31 -12v0zM128 640h768q19 0 31 11.5t12 31.5q0 19 -12 30.5t-31 11.5h-768q-19 0 -31 -11.5t-12 -30.5q0 -20 12 -31.5t31 -11.5zM896 384h-768q-19 0 -31 -11.5t-12 -31.5
+q0 -19 12 -30.5t31 -11.5h768q19 0 31 11.5t12 30.5q0 20 -12 31.5t-31 11.5zM725 213h-597q-19 0 -31 -11.5t-12 -30.5q0 -20 12 -31.5t31 -11.5h597q20 0 31.5 11.5t11.5 31.5q0 19 -11.5 30.5t-31.5 11.5z" />
+    <glyph glyph-name="uniE908" unicode="&#xe908;" 
+d="M896 555h-597q-20 0 -31.5 -12t-11.5 -31t11.5 -31t31.5 -12h597q19 0 31 12t12 31t-12 31t-31 12zM128 640h768q19 0 31 11.5t12 31.5q0 19 -12 30.5t-31 11.5h-768q-19 0 -31 -11.5t-12 -30.5q0 -20 12 -31.5t31 -11.5zM896 384h-768q-19 0 -31 -11.5t-12 -31.5
+q0 -19 12 -30.5t31 -11.5h768q19 0 31 11.5t12 30.5q0 20 -12 31.5t-31 11.5zM896 213h-597q-20 0 -31.5 -11.5t-11.5 -30.5q0 -20 11.5 -31.5t31.5 -11.5h597q19 0 31 11.5t12 31.5q0 19 -12 30.5t-31 11.5z" />
+    <glyph glyph-name="uniE909" unicode="&#xe909;" 
+d="M939 469h-128q-20 0 -31.5 -11.5t-11.5 -30.5q0 -20 11.5 -31.5t31.5 -11.5h81q-8 -67 -37 -126t-74 -103.5t-103 -72.5q-58 -29 -123 -35v512q54 13 91 59.5t37 106.5q0 71 -50.5 121t-120.5 50t-120.5 -50t-50.5 -121q0 -60 37 -105t91 -61v-516q-67 8 -126 37
+q-58 29 -103 73.5t-73 102.5t-35 124h81q20 0 31.5 11.5t11.5 30.5q0 20 -11.5 31.5t-31.5 11.5h-128q-19 0 -30.5 -11.5t-11.5 -31.5q0 -97 36 -183q37 -85 100.5 -148.5t149.5 -100.5q85 -37 183 -37t183 37q86 37 149.5 100.5t100.5 148.5q36 86 36 183q0 23 -11.5 35
+t-30.5 12v0zM427 725q0 36 25 61t60 25t60 -25t25 -61q0 -35 -25 -60t-60 -25t-60 25t-25 60v0z" />
+    <glyph glyph-name="uniE90A" unicode="&#xe90a;" 
+d="M892 154q41 57 65 126.5t24 146.5q0 48 -9.5 95t-28.5 88v2v2q-24 56 -62 104q-37 47 -85.5 83t-106.5 59q-58 24 -122 32h-2h-2q-13 3 -25.5 3.5t-25.5 0.5q-58 0 -111 -13q-54 -13 -102 -37.5t-89 -59.5t-73 -78q0 -3 -2.5 -4t-2.5 -4q-41 -58 -65 -127.5t-24 -145.5
+q0 -48 9.5 -95.5t28.5 -92.5v-2v-2q24 -55 62 -102q37 -47 85 -83.5t105 -60.5t119 -32h2.5h2.5q12 0 25 -2t26 -2q57 0 111 13t102 37.5t89 59.5t73 78l6 6t3 7v0zM585 299h-150l-72 128l72 128h150l72 -128zM128 427q0 44 9.5 88t28.5 82l171 -298h-188q-9 28 -15 60
+t-6 68v0zM683 555h187q10 -29 16 -61t6 -67q0 -45 -9.5 -89t-29.5 -82l-170 299v0zM832 640h-346l94 162q80 -13 145 -55.5t107 -106.5zM486 811l-170 -299l-94 162q48 58 116 94t148 43v0zM192 213h346l-94 -162q-80 13 -145 55.5t-107 106.5zM538 43l170 298l94 -162
+q-48 -57 -116 -93.5t-148 -42.5v0z" />
+    <glyph glyph-name="uniE90B" unicode="&#xe90b;" 
+d="M841 457q-13 12 -30 12t-30 -12l-226 -227v495q0 20 -12 31.5t-31 11.5t-31 -11.5t-12 -31.5v-495l-226 227q-13 12 -30 12t-30 -12q-12 -13 -12 -30t12 -30l299 -299q3 -3 6.5 -5.5t6.5 -2.5q3 -4 8.5 -4.5t8.5 -0.5t8.5 0.5t8.5 4.5q3 3 6.5 4t6.5 4l299 299
+q12 13 12 30t-12 30v0z" />
+    <glyph glyph-name="uniE90C" unicode="&#xe90c;" 
+d="M512 896q-98 0 -183 -37q-86 -36 -149.5 -100t-100.5 -149q-36 -86 -36 -183q0 -98 36 -183q37 -86 100.5 -149.5t149.5 -100.5q85 -37 183 -37t183 37q86 37 149.5 100.5t100.5 149.5q36 85 36 183q0 97 -36 183q-37 85 -100.5 149t-149.5 100q-85 37 -183 37zM512 43
+q-80 0 -150 30t-122 82t-82 122t-30 150t30 150t82 122t122 82t150 30t150 -30t122 -82t82 -122t30 -150t-30 -150t-82 -122t-122 -82t-150 -30zM653 457l-98 -99v239q0 20 -12 31.5t-31 11.5t-31 -11.5t-12 -31.5v-239l-98 99q-13 12 -30 12t-30 -12q-12 -13 -12 -30
+t12 -30l171 -171q3 -3 6.5 -5.5t6.5 -2.5q3 -4 8.5 -4.5t8.5 -0.5t8.5 0.5t8.5 4.5q3 3 6.5 4t6.5 4l171 171q12 13 12 30t-12 30q-13 12 -30 12t-30 -12v0z" />
+    <glyph glyph-name="uniE90D" unicode="&#xe90d;" 
+d="M725 256h-324l354 354q13 13 13 30t-13 30t-30 13t-30 -13l-354 -354v324q0 19 -11.5 31t-30.5 12q-20 0 -31.5 -12t-11.5 -31v-427q0 -3 0.5 -8.5t3.5 -8.5q3 -6 9 -12t13 -9q3 -3 8 -3.5t9 -0.5h426q20 0 31.5 11.5t11.5 30.5q0 20 -11.5 31.5t-31.5 11.5v0z" />
+    <glyph glyph-name="uniE90E" unicode="&#xe90e;" 
+d="M768 640q0 19 -11.5 31t-31.5 12q-19 0 -30.5 -12t-11.5 -31v-324l-354 354q-13 13 -30 13t-30 -13t-13 -30t13 -30l354 -354h-324q-20 0 -31.5 -11.5t-11.5 -31.5q0 -19 11.5 -30.5t31.5 -11.5h426q4 0 9 0.5t8 3.5q7 3 13 9t9 12q3 3 3.5 8.5t0.5 8.5v427z" />
+    <glyph glyph-name="uniE90F" unicode="&#xe90f;" 
+d="M811 469h-495l226 226q13 13 13 30t-13 30t-30 13t-30 -13l-299 -298q-3 -4 -5.5 -7t-2.5 -6q-3 -7 -3 -16t3 -18q3 -4 4 -7t4 -6l299 -299q7 -6 15.5 -9.5t14.5 -3.5t14.5 3.5t15.5 9.5q13 13 13 30t-13 30l-226 226h495q19 0 30.5 11.5t11.5 31.5q0 19 -11.5 30.5
+t-30.5 11.5v0z" />
+    <glyph glyph-name="uniE910" unicode="&#xe910;" 
+d="M512 896q-98 0 -183 -37q-86 -36 -149.5 -100t-100.5 -149q-36 -86 -36 -183q0 -98 36 -183q37 -86 100.5 -149.5t149.5 -100.5q85 -37 183 -37t183 37q86 37 149.5 100.5t100.5 149.5q36 85 36 183q0 97 -36 183q-37 85 -100.5 149t-149.5 100q-85 37 -183 37zM512 43
+q-80 0 -150 30t-122 82t-82 122t-30 150t30 150t82 122t122 82t150 30t150 -30t122 -82t82 -122t30 -150t-30 -150t-82 -122t-122 -82t-150 -30zM683 469h-239l98 98q13 13 13 30t-13 30t-30 13t-30 -13l-171 -170q-3 -4 -5.5 -7t-2.5 -6q-3 -7 -3 -16t3 -18q3 -4 4 -7t4 -6
+l171 -171q7 -6 15.5 -9.5t14.5 -3.5t14.5 3.5t15.5 9.5q13 13 13 30t-13 30l-98 98h239q19 0 30.5 11.5t11.5 31.5q0 19 -11.5 30.5t-30.5 11.5v0z" />
+    <glyph glyph-name="uniE911" unicode="&#xe911;" 
+d="M849 410q3 6 3 15t-3 19q-3 3 -4 6t-4 7l-299 298q-13 13 -30 13t-30 -13t-13 -30t13 -30l226 -226h-495q-19 0 -30.5 -11.5t-11.5 -30.5q0 -20 11.5 -31.5t30.5 -11.5h495l-226 -226q-13 -13 -13 -30t13 -30q7 -6 15.5 -9.5t14.5 -3.5t14.5 3.5t15.5 9.5l299 299
+q3 3 5.5 6t2.5 7v0z" />
+    <glyph glyph-name="uniE912" unicode="&#xe912;" 
+d="M512 896q-98 0 -183 -37q-86 -36 -149.5 -100t-100.5 -149q-36 -86 -36 -183q0 -98 36 -183q37 -86 100.5 -149.5t149.5 -100.5q85 -37 183 -37t183 37q86 37 149.5 100.5t100.5 149.5q36 85 36 183q0 97 -36 183q-37 85 -100.5 149t-149.5 100q-85 37 -183 37zM512 43
+q-80 0 -150 30t-122 82t-82 122t-30 150t30 150t82 122t122 82t150 30t150 -30t122 -82t82 -122t30 -150t-30 -150t-82 -122t-122 -82t-150 -30zM721 444q-3 3 -4 6t-4 7l-171 170q-13 13 -30 13t-30 -13t-13 -30t13 -30l98 -98h-239q-19 0 -30.5 -11.5t-11.5 -30.5
+q0 -20 11.5 -31.5t30.5 -11.5h239l-98 -98q-13 -13 -13 -30t13 -30q7 -6 15.5 -9.5t14.5 -3.5t14.5 3.5t15.5 9.5l171 171q3 3 5.5 6t2.5 7q3 9 3 16.5t-3 17.5v0z" />
+    <glyph glyph-name="uniE913" unicode="&#xe913;" 
+d="M841 457l-299 298q-3 3 -6.5 6t-6.5 3q-6 3 -15 3t-19 -3q-3 -3 -6.5 -4.5t-6.5 -4.5l-299 -298q-12 -13 -12 -30t12 -30q13 -13 30 -13t30 13l226 226v-495q0 -19 12 -31t31 -12t31 12t12 31v495l226 -226q6 -7 14.5 -10t15.5 -3q6 0 14.5 3t15.5 10q12 13 12 30t-12 30
+v0z" />
+    <glyph glyph-name="uniE914" unicode="&#xe914;" 
+d="M512 896q-98 0 -183 -37q-86 -36 -149.5 -100t-100.5 -149q-36 -86 -36 -183q0 -98 36 -183q37 -86 100.5 -149.5t149.5 -100.5q85 -37 183 -37t183 37q86 37 149.5 100.5t100.5 149.5q36 85 36 183q0 97 -36 183q-37 85 -100.5 149t-149.5 100q-85 37 -183 37zM512 43
+q-80 0 -150 30t-122 82t-82 122t-30 150t30 150t82 122t122 82t150 30t150 -30t122 -82t82 -122t30 -150t-30 -150t-82 -122t-122 -82t-150 -30zM542 627q-3 3 -6.5 6t-6.5 3q-6 3 -15 3t-19 -3q-3 -3 -6.5 -4.5t-6.5 -4.5l-171 -170q-12 -13 -12 -30t12 -30q13 -13 30 -13
+t30 13l98 98v-239q0 -19 12 -31t31 -12t31 12t12 31v239l98 -98q6 -7 14.5 -10t15.5 -3q6 0 14.5 3t15.5 10q12 13 12 30t-12 30z" />
+    <glyph glyph-name="uniE915" unicode="&#xe915;" 
+d="M401 597h324q20 0 31.5 12t11.5 31t-11.5 31t-31.5 12h-426q-4 0 -9 -0.5t-8 -4.5q-7 -3 -13 -9t-9 -12q-3 -3 -3.5 -8.5t-0.5 -8.5v-427q0 -19 11.5 -30.5t31.5 -11.5q19 0 30.5 11.5t11.5 30.5v325l354 -355q7 -6 15.5 -9t14.5 -3q7 0 15.5 3t14.5 9q13 13 13 30
+t-13 30l-354 354v0z" />
+    <glyph glyph-name="uniE916" unicode="&#xe916;" 
+d="M764 657q-3 6 -9 12t-13 9q-3 4 -8 4.5t-9 0.5h-426q-20 0 -31.5 -12t-11.5 -31t11.5 -31t31.5 -12h324l-354 -354q-13 -13 -13 -30t13 -30q6 -6 13 -9t17 -3q9 0 16 3t14 9l354 355v-325q0 -19 11.5 -30.5t30.5 -11.5q20 0 31.5 11.5t11.5 30.5v427q0 3 -0.5 8.5
+t-3.5 8.5v0z" />
+    <glyph glyph-name="uniE917" unicode="&#xe917;" 
+d="M512 896q-46 0 -92 -9q-45 -8 -87.5 -25.5t-80.5 -42.5q-39 -26 -73 -60q-33 -33 -59 -72q-25 -39 -42.5 -81.5t-26.5 -87.5q-8 -45 -8 -91q0 -98 36 -183q37 -86 100.5 -149.5t149.5 -100.5q85 -37 183 -37q77 0 151 25t135 73q13 10 15.5 28.5t-7.5 31.5
+q-9 13 -28 15.5t-31 -6.5q-52 -39 -111.5 -60t-123.5 -21q-80 0 -150 30t-122 82t-82 122t-30 150q0 76 28.5 147t82.5 126q54 54 125 82.5t148 28.5q80 0 150 -30t122 -82t82 -122t30 -150v-43q0 -35 -25 -60t-60 -25q-36 0 -61 25t-25 60v213q0 20 -11.5 31.5t-30.5 11.5
+q-20 0 -31.5 -11.5t-11.5 -31.5v0q-26 20 -59.5 31.5t-68.5 11.5q-45 0 -84 -17q-39 -16 -67.5 -45t-45.5 -68q-16 -39 -16 -83q0 -45 16 -84q17 -39 45.5 -68t67.5 -45q39 -17 84 -17q48 0 88.5 20.5t69.5 52.5q22 -32 59 -52.5t82 -20.5q70 0 120 50.5t50 120.5v43
+q0 97 -36 183q-37 85 -100.5 149t-149.5 100q-85 37 -183 37v0zM512 299q-54 0 -91 36.5t-37 91.5q0 54 37 91t91 37t91 -37t37 -91q0 -55 -37 -91.5t-91 -36.5z" />
+    <glyph glyph-name="uniE918" unicode="&#xe918;" 
+d="M853 597q0 71 -27 133t-73 108.5t-108 73.5q-63 27 -133 27t-133 -27q-62 -27 -108 -73.5t-73 -108.5t-27 -133q0 -83 35.5 -153.5t96.5 -114.5l-47 -367q-3 -13 2 -25t15 -18t22 -6t25 6l192 115l192 -115q3 -3 9 -3.5t12 -0.5q7 0 12.5 2.5t9.5 5.5q3 3 11.5 16.5
+t5.5 26.5l-47 363q64 44 100 114.5t36 153.5v0zM256 597q0 53 20 100q20 46 55 81t82 55q46 20 99 20t99 -20q47 -20 82 -55t55 -81q20 -47 20 -100q0 -52 -20 -99t-55 -81.5t-82 -54.5q-46 -21 -99 -21t-99 21q-47 20 -82 54.5t-55 81.5t-20 99zM670 38l-137 81q-9 7 -21 7
+t-21 -7l-137 -81l30 244q29 -13 61 -19.5t67 -6.5t67 6.5t61 19.5l30 -244v0z" />
+    <glyph glyph-name="uniE919" unicode="&#xe919;" 
+d="M512 555q-19 0 -31 -12t-12 -31v-427q0 -19 12 -30.5t31 -11.5t31 11.5t12 30.5v427q0 19 -12 31t-31 12v0zM768 811q-19 0 -31 -12t-12 -31v-683q0 -19 12 -30.5t31 -11.5t31 11.5t12 30.5v683q0 19 -12 31t-31 12v0zM256 299q-19 0 -31 -12t-12 -31v-171
+q0 -19 12 -30.5t31 -11.5t31 11.5t12 30.5v171q0 19 -12 31t-31 12z" />
+    <glyph glyph-name="uniE91A" unicode="&#xe91a;" 
+d="M768 555q-19 0 -31 -12t-12 -31v-427q0 -19 12 -30.5t31 -11.5t31 11.5t12 30.5v427q0 19 -12 31t-31 12v0zM512 811q-19 0 -31 -12t-12 -31v-683q0 -19 12 -30.5t31 -11.5t31 11.5t12 30.5v683q0 19 -12 31t-31 12v0zM256 384q-19 0 -31 -11.5t-12 -31.5v-256
+q0 -19 12 -30.5t31 -11.5t31 11.5t12 30.5v256q0 20 -12 31.5t-31 11.5z" />
+    <glyph glyph-name="uniE91B" unicode="&#xe91b;" 
+d="M725 725h-597q-54 0 -91 -36.5t-37 -91.5v-341q0 -54 37 -91t91 -37h597q55 0 91.5 37t36.5 91v341q0 55 -36.5 91.5t-91.5 36.5v0zM768 256q0 -19 -11.5 -31t-31.5 -12h-597q-19 0 -31 12t-12 31v341q0 20 12 31.5t31 11.5h597q20 0 31.5 -11.5t11.5 -31.5v-341v0z
+M981 512q-19 0 -30.5 -11.5t-11.5 -31.5v-85q0 -19 11.5 -31t30.5 -12q20 0 31.5 12t11.5 31v85q0 20 -11.5 31.5t-31.5 11.5v0z" />
+    <glyph glyph-name="uniE91C" unicode="&#xe91c;" 
+d="M725 725h-85q-19 0 -31 -11.5t-12 -30.5q0 -20 12 -31.5t31 -11.5h85q20 0 31.5 -11.5t11.5 -31.5v-341q0 -19 -11.5 -31t-31.5 -12h-136q-19 0 -31 -11.5t-12 -30.5q0 -20 12 -31.5t31 -11.5h136q55 0 91.5 37t36.5 91v341q0 55 -36.5 91.5t-91.5 36.5v0zM213 213h-85
+q-19 0 -31 12t-12 31v341q0 20 12 31.5t31 11.5h137q19 0 30.5 11.5t11.5 31.5q0 19 -11.5 30.5t-30.5 11.5h-137q-54 0 -91 -36.5t-37 -91.5v-341q0 -54 37 -91t91 -37h85q20 0 31.5 11.5t11.5 31.5q0 19 -11.5 30.5t-31.5 11.5v0zM981 512q-19 0 -30.5 -11.5t-11.5 -31.5
+v-85q0 -19 11.5 -31t30.5 -12q20 0 31.5 12t11.5 31v85q0 20 -11.5 31.5t-31.5 11.5v0zM593 448q-6 10 -16 15.5t-22 5.5h-175l128 188q9 16 6 33t-19 27q-16 9 -33 6t-27 -19l-170 -256q-7 -10 -7 -21.5t7 -21.5q3 -9 12 -15t22 -6h175l-128 -188q-10 -16 -7 -33t19 -26
+q7 -4 13.5 -6.5t12.5 -2.5q10 0 19 5.5t15 11.5l171 256q6 13 8 23.5t-4 23.5v0z" />
+    <glyph glyph-name="uniE91D" unicode="&#xe91d;" 
+d="M606 81q-16 10 -33 4.5t-27 -21.5q-9 -16 -26.5 -19.5t-33.5 2.5q-6 3 -9.5 7t-7.5 10q-9 16 -26 19.5t-33 -2.5q-16 -10 -20 -27t3 -33q9 -16 19.5 -26.5t26.5 -20.5q16 -9 32 -13t32 -4q32 0 62 16t49 48q16 16 12 33t-20 27zM939 256q-36 0 -61 25t-25 60v214
+q0 70 -27 132t-73 108.5t-108 73.5q-63 27 -133 27t-133 -27q-62 -27 -108 -73.5t-73 -108.5t-27 -132v-214q0 -35 -25 -60t-61 -25q-19 0 -30.5 -11.5t-11.5 -31.5q0 -19 11.5 -30.5t30.5 -11.5h854q19 0 30.5 11.5t11.5 30.5q0 20 -11.5 31.5t-30.5 11.5v0zM235 256
+q9 19 15 41t6 44v214q0 52 20 99t55 81.5t82 54.5q46 21 99 21t99 -21q47 -20 82 -54.5t55 -81.5t20 -99v-214q0 -22 6 -44t15 -41h-554v0z" />
+    <glyph glyph-name="uniE91E" unicode="&#xe91e;" 
+d="M388 777q47 25 97 30q50 6 97.5 -7.5t87.5 -43.5q41 -31 68 -78q13 -25 21.5 -58t8.5 -65v-171q0 -19 11.5 -31t31.5 -12q19 0 30.5 12t11.5 31v171q0 44 -11.5 86t-30.5 80q-36 61 -90 102q-54 40 -117 58t-131 10q-67 -7 -127 -42q-16 -6 -21.5 -22.5t4.5 -32.5
+q9 -16 26 -20t33 3zM606 81q-16 10 -33 4.5t-27 -21.5q-9 -16 -26.5 -19.5t-33.5 2.5q-6 3 -9.5 7t-7.5 10q-9 16 -26 19.5t-33 -2.5q-16 -10 -20 -27t3 -33q9 -16 19.5 -26.5t26.5 -20.5q16 -9 32 -13t32 -4q32 0 62 16t49 48q16 16 12 33t-20 27zM1011 -13l-256 256v0v0
+l-682 683q-13 13 -30 13t-30 -13t-13 -30t13 -30l183 -183q-13 -32 -19 -64t-6 -64v-214q0 -35 -25 -60t-61 -25q-19 0 -30.5 -11.5t-11.5 -31.5q0 -19 11.5 -30.5t30.5 -11.5h623l243 -244q7 -6 15.5 -9t14.5 -3q7 0 15.5 3t14.5 9q13 13 13 30t-13 30v0zM235 256
+q9 19 15 41t6 44v214q0 16 2.5 31.5t6.5 27.5l358 -358h-388v0z" />
+    <glyph glyph-name="uniE91F" unicode="&#xe91f;" 
+d="M572 427l205 204q12 13 12 30t-12 30l-235 235q-10 9 -22 12t-25 -4q-13 -3 -19.5 -14t-6.5 -24v-367l-162 162q-13 13 -30 13t-30 -13q-12 -13 -12 -30t12 -30l205 -204l-205 -205q-12 -13 -12 -30t12 -30q13 -13 30 -13t30 13l162 162v-367q0 -12 6.5 -23.5t19.5 -14.5
+q3 -3 8.5 -3.5t8.5 -0.5q10 0 16.5 3t13.5 9l235 235q12 13 12 30t-12 30l-205 205v0zM555 794l132 -133l-132 -132v265v0zM555 60v264l132 -132l-132 -132v0z" />
+    <glyph glyph-name="uniE920" unicode="&#xe920;" 
+d="M742 444q32 29 50.5 69t18.5 84q0 45 -17 84q-16 39 -45 68t-68 45q-39 17 -84 17h-341q-19 0 -31 -12t-12 -31v-683q0 -19 12 -30.5t31 -11.5h384q45 0 84 16q39 17 67.5 45.5t45.5 67.5q16 39 16 84q0 61 -31.5 110t-79.5 78v0zM299 725h298q55 0 91.5 -36.5
+t36.5 -91.5q0 -54 -36.5 -91t-91.5 -37h-298v256zM640 128h-341v256h341q54 0 91 -37t37 -91t-37 -91t-91 -37z" />
+    <glyph glyph-name="uniE921" unicode="&#xe921;" 
+d="M853 896h-576q-60 0 -104.5 -44.5t-44.5 -104.5v-640q0 -61 44.5 -105.5t104.5 -44.5h576q20 0 31.5 12t11.5 31v853q0 20 -11.5 31.5t-31.5 11.5v0zM277 811h534v-555h-534q-16 0 -32 -3.5t-32 -13.5v508q0 25 19.5 44.5t44.5 19.5v0zM277 43q-25 0 -44.5 19t-19.5 45
+q0 25 19.5 44.5t44.5 19.5h534v-128h-534v0z" />
+    <glyph glyph-name="uniE922" unicode="&#xe922;" 
+d="M939 853h-256q-52 0 -97 -23.5t-74 -61.5q-29 38 -74 61.5t-97 23.5h-256q-19 0 -30.5 -11.5t-11.5 -30.5v-640q0 -20 11.5 -31.5t30.5 -11.5h299q35 0 60 -25t25 -60q0 -20 12 -31.5t31 -11.5t31 11.5t12 31.5q0 35 25 60t60 25h299q19 0 30.5 11.5t11.5 31.5v640
+q0 19 -11.5 30.5t-30.5 11.5v0zM469 192q-19 10 -41 15.5t-44 5.5h-256v555h213q55 0 91.5 -37t36.5 -91v-448v0zM896 213h-256q-22 0 -44 -5.5t-41 -15.5v448q0 54 36.5 91t91.5 37h213v-555z" />
+    <glyph glyph-name="uniE923" unicode="&#xe923;" 
+d="M725 853h-426q-55 0 -91.5 -36.5t-36.5 -91.5v-682q0 -13 5.5 -22.5t15.5 -16.5q10 -6 21.5 -4t21.5 9l273 196l273 -196q6 -4 12.5 -6.5t12.5 -2.5q7 0 11 0.5t11 3.5q9 7 15 16.5t6 22.5v682q3 55 -33 91.5t-91 36.5v0zM768 124l-230 166q-7 3 -13.5 6t-12.5 3
+t-12.5 -3t-13.5 -6l-230 -166v601q0 20 11.5 31.5t31.5 11.5h426q20 0 31.5 -11.5t11.5 -31.5v-601v0z" />
+    <glyph glyph-name="uniE924" unicode="&#xe924;" 
+d="M909 742l-342 171v0v0q-25 13 -55.5 13t-59.5 -13l-341 -171q-32 -16 -50 -45.5t-18 -65.5v-405q0 -35 18.5 -67t53.5 -48l342 -171q12 -6 27 -9.5t28 -3.5q16 0 29.5 3.5t25.5 9.5l342 171q32 16 52 46.5t20 68.5v405q0 36 -18.5 65.5t-53.5 45.5zM495 841q3 3 8.5 3.5
+t8.5 0.5q6 0 10 -0.5t7 -3.5l316 -158l-333 -167l-333 167l316 158v0zM149 183q-9 7 -15 18t-6 21v392l341 -170v-418l-320 157v0zM870 183l-315 -157v418l341 170v-392q0 -13 -6.5 -22.5t-19.5 -16.5z" />
+    <glyph glyph-name="uniE925" unicode="&#xe925;" 
+d="M853 683h-128v42q0 55 -36.5 91.5t-91.5 36.5h-170q-55 0 -91.5 -36.5t-36.5 -91.5v-42h-128q-55 0 -91.5 -37t-36.5 -91v-427q0 -54 36.5 -91t91.5 -37h682q55 0 91.5 37t36.5 91v427q0 54 -36.5 91t-91.5 37zM384 725q0 20 11.5 31.5t31.5 11.5h170q20 0 31.5 -11.5
+t11.5 -31.5v-42h-256v42v0zM640 597v-512h-256v512h256zM128 128v427q0 19 11.5 30.5t31.5 11.5h128v-512h-128q-20 0 -31.5 12t-11.5 31v0zM896 128q0 -19 -11.5 -31t-31.5 -12h-128v512h128q20 0 31.5 -11.5t11.5 -30.5v-427v0z" />
+    <glyph glyph-name="uniE926" unicode="&#xe926;" 
+d="M811 811h-86v42q0 20 -11.5 31.5t-30.5 11.5q-20 0 -31.5 -11.5t-11.5 -31.5v-42h-256v42q0 20 -11.5 31.5t-31.5 11.5q-19 0 -30.5 -11.5t-11.5 -31.5v-42h-86q-54 0 -91 -37t-37 -91v-598q0 -54 37 -91t91 -37h598q54 0 91 37t37 91v598q0 54 -37 91t-91 37v0zM213 725
+h86v-42q0 -20 11.5 -31.5t30.5 -11.5q20 0 31.5 11.5t11.5 31.5v42h256v-42q0 -20 11.5 -31.5t31.5 -11.5q19 0 30.5 11.5t11.5 31.5v42h86q19 0 30.5 -11.5t11.5 -30.5v-128h-682v128q0 19 11.5 30.5t30.5 11.5v0zM811 43h-598q-19 0 -30.5 11.5t-11.5 30.5v384h682v-384
+q0 -19 -11.5 -30.5t-30.5 -11.5z" />
+    <glyph glyph-name="uniE927" unicode="&#xe927;" 
+d="M896 725h-149l-73 111q-6 7 -15 12t-19 5h-256q-10 0 -19 -5t-15 -12l-73 -111h-149q-54 0 -91 -36.5t-37 -91.5v-469q0 -54 37 -91t91 -37h768q54 0 91 37t37 91v469q0 55 -37 91.5t-91 36.5v0zM939 128q0 -19 -12 -31t-31 -12h-768q-19 0 -31 12t-12 31v469
+q0 20 12 31.5t31 11.5h171q9 0 18 5.5t16 11.5l72 111h209l73 -111q10 -6 19.5 -11.5t18.5 -5.5h171q19 0 31 -11.5t12 -31.5v-469v0zM512 597q-45 0 -84 -16q-39 -17 -67.5 -45.5t-45.5 -67.5q-16 -39 -16 -84t16 -84q17 -39 45.5 -67.5t67.5 -45.5q39 -16 84 -16t84 16
+q39 17 67.5 45.5t45.5 67.5q16 39 16 84t-16 84q-17 39 -45.5 67.5t-67.5 45.5q-39 16 -84 16zM512 256q-54 0 -91 37t-37 91t37 91t91 37t91 -37t37 -91t-37 -91t-91 -37z" />
+    <glyph glyph-name="uniE928" unicode="&#xe928;" 
+d="M926 73v0v0l-640 640v0v0l-213 213q-13 13 -30 13t-30 -13t-13 -30t13 -30l141 -141h-26q-54 0 -91 -36.5t-37 -91.5v-469q0 -54 37 -91t91 -37h751l72 -73q7 -6 15.5 -9t14.5 -3q7 0 15.5 3t14.5 9q13 13 13 30t-13 30l-85 86v0zM405 457q4 3 5 4l4 4l179 -179
+q-13 -10 -28.5 -18.5t-31.5 -11.5q-25 -3 -49.5 1t-44.5 20q-22 16 -35.5 36t-19.5 45q-3 26 3 51t18 48v0zM128 85q-19 0 -31 12t-12 31v469q0 20 12 31.5t31 11.5h111l115 -115q-6 -7 -10 -11t-7 -11q-26 -35 -33 -75.5t-1 -81.5q6 -42 30 -78.5t60 -58.5q25 -19 56 -28.5
+t63 -9.5q10 0 19.5 0.5t18.5 3.5q29 6 54.5 19t48.5 32l141 -141h-666v0zM896 725h-149l-73 111q-6 7 -15 12t-19 5h-256q-19 0 -31 -11.5t-12 -30.5q0 -20 12 -31.5t31 -11.5h235l72 -111q7 -6 16 -11.5t18 -5.5h171q19 0 31 -11.5t12 -31.5v-396q0 -20 11.5 -31.5
+t30.5 -11.5q20 0 31.5 11.5t11.5 31.5v396q0 55 -37 91.5t-91 36.5v0z" />
+    <glyph glyph-name="uniE929" unicode="&#xe929;" 
+d="M90 465q-16 3 -30 -8t-17 -30q-4 -16 7.5 -30t30.5 -17q59 -7 111 -32t92 -65t66 -93q26 -52 34 -113q0 -13 11 -23.5t27 -10.5h2.5h2.5q16 3 27 17t11 30q-8 75 -39 140q-32 65 -81.5 114.5t-114.5 81.5q-65 31 -140 39v0zM94 294q-16 4 -32 -7t-19 -27q-4 -16 5.5 -32
+t28.5 -19q51 -10 86.5 -45.5t45.5 -86.5q3 -16 15 -25t28 -9h4h4q16 3 27 17.5t7 33.5q-16 77 -69.5 130.5t-130.5 69.5v0zM853 811h-682q-55 0 -91.5 -37t-36.5 -91v-86q0 -19 11.5 -30.5t30.5 -11.5q20 0 31.5 11.5t11.5 30.5v86q0 19 11.5 30.5t31.5 11.5h682
+q20 0 31.5 -11.5t11.5 -30.5v-512q0 -20 -11.5 -31.5t-31.5 -11.5h-256q-19 0 -30.5 -11.5t-11.5 -31.5q0 -19 11.5 -30.5t30.5 -11.5h256q55 0 91.5 36.5t36.5 91.5v512q0 54 -36.5 91t-91.5 37v0zM55 115q-6 -6 -9 -13t-3 -17q0 -9 3 -16t9 -14q7 -6 14 -9t16 -3
+q10 0 17 3t13 9q7 7 10 14t3 16q0 10 -3 17t-10 13q-13 13 -30 13t-30 -13v0z" />
+    <glyph glyph-name="uniE92A" unicode="&#xe92a;" 
+d="M883 713q-13 12 -30 12t-30 -12l-439 -440l-183 184q-13 12 -30 12t-30 -12q-13 -13 -13 -30t13 -30l213 -214q7 -6 13.5 -9t16.5 -3t16.5 3t13.5 9l469 470q13 13 13 30t-13 30v0z" />
+    <glyph glyph-name="uniE92B" unicode="&#xe92b;" 
+d="M939 508q-20 0 -31.5 -12t-11.5 -31v-38q0 -80 -30 -150t-82 -122t-122 -82t-150 -30v0v0q-80 0 -150 30t-122 82t-82 122t-30 150t30 150t82 122t122 82t150 30v0v0q42 0 80.5 -9t77.5 -25q16 -7 32.5 -1t22.5 22q7 16 1 32.5t-22 22.5q-45 20 -93 31.5t-99 11.5v0v0
+q-98 0 -183 -37q-86 -36 -149.5 -100t-100.5 -149q-36 -86 -36 -183q0 -98 36 -183q37 -86 100.5 -149.5t149.5 -100.5q85 -37 183 -37v0v0q98 0 183 37q86 37 149.5 100.5t100.5 149.5q36 85 36 183v38q0 19 -11.5 31t-30.5 12v0zM414 499q-13 13 -30 13t-30 -13t-13 -30
+t13 -30l128 -128q7 -6 13.5 -9t16.5 -3t16.5 3t13.5 9l427 427q12 13 12 30t-12 30q-13 13 -30 13t-30 -13l-397 -397l-98 98v0z" />
+    <glyph glyph-name="uniE92C" unicode="&#xe92c;" 
+d="M969 798q-13 13 -30 13t-30 -13l-397 -397l-98 98q-13 13 -30 13t-30 -13t-13 -30t13 -30l128 -128q7 -6 15.5 -9t14.5 -3t14.5 3t15.5 9l427 427q12 13 12 30t-12 30v0zM896 469q-19 0 -31 -11.5t-12 -30.5v-299q0 -19 -11.5 -31t-30.5 -12h-598q-19 0 -30.5 12
+t-11.5 31v597q0 20 11.5 31.5t30.5 11.5h470q19 0 30.5 11.5t11.5 31.5q0 19 -11.5 30.5t-30.5 11.5h-470q-54 0 -91 -36.5t-37 -91.5v-597q0 -54 37 -91t91 -37h598q54 0 91 37t37 91v299q0 19 -12 30.5t-31 11.5v0z" />
+    <glyph glyph-name="uniE92D" unicode="&#xe92d;" 
+d="M798 585q-13 12 -30 12t-30 -12l-226 -227l-226 227q-13 12 -30 12t-30 -12q-13 -13 -13 -30t13 -30l256 -256q7 -7 15.5 -10t14.5 -3t14.5 3t15.5 10l256 256q13 13 13 30t-13 30v0z" />
+    <glyph glyph-name="uniE92E" unicode="&#xe92e;" 
+d="M444 427l226 226q13 13 13 30t-13 30q-13 12 -30 12t-30 -12l-256 -256q-13 -13 -13 -30t13 -30l256 -256q7 -7 15.5 -10t14.5 -3t14.5 3t15.5 10q13 13 13 30t-13 30l-226 226v0z" />
+    <glyph glyph-name="uniE92F" unicode="&#xe92f;" 
+d="M670 457l-256 256q-13 12 -30 12t-30 -12q-13 -13 -13 -30t13 -30l226 -226l-226 -226q-13 -13 -13 -30t13 -30q7 -7 13.5 -10t16.5 -3t16.5 3t13.5 10l256 256q13 13 13 30t-13 30v0z" />
+    <glyph glyph-name="uniE930" unicode="&#xe930;" 
+d="M798 329l-256 256q-13 12 -30 12t-30 -12l-256 -256q-13 -13 -13 -30t13 -30t30 -13t30 13l226 226l226 -226q7 -7 15.5 -10t14.5 -3t14.5 3t15.5 10q13 13 13 30t-13 30z" />
+    <glyph glyph-name="uniE931" unicode="&#xe931;" 
+d="M695 414l-183 -184l-183 184q-13 13 -30 13t-30 -13t-13 -30t13 -30l213 -213q7 -7 15.5 -10t14.5 -3t14.5 3t15.5 10l213 213q13 13 13 30t-13 30t-30 13t-30 -13zM482 439q7 -6 15.5 -9t14.5 -3t14.5 3t15.5 9l213 214q13 13 13 30t-13 30q-13 12 -30 12t-30 -12
+l-183 -184l-183 184q-13 12 -30 12t-30 -12q-13 -13 -13 -30t13 -30l213 -214v0z" />
+    <glyph glyph-name="uniE932" unicode="&#xe932;" 
+d="M316 427l183 183q13 13 13 30t-13 30t-30 13t-30 -13l-213 -213q-13 -13 -13 -30t13 -30l213 -214q7 -6 15.5 -9t14.5 -3q7 0 15.5 3t14.5 9q13 13 13 30t-13 30l-183 184v0zM614 427l184 183q13 13 13 30t-13 30t-30 13t-30 -13l-213 -213q-13 -13 -13 -30t13 -30
+l213 -214q7 -6 15.5 -9t14.5 -3t14.5 3t15.5 9q13 13 13 30t-13 30l-184 184v0z" />
+    <glyph glyph-name="uniE933" unicode="&#xe933;" 
+d="M798 457l-213 213q-13 13 -30 13t-30 -13t-13 -30t13 -30l183 -183l-183 -184q-13 -13 -13 -30t13 -30q6 -6 14.5 -9t15.5 -3q6 0 14.5 3t15.5 9l213 214q13 13 13 30t-13 30v0zM499 457l-213 213q-13 13 -30 13t-30 -13t-13 -30t13 -30l184 -183l-184 -184
+q-13 -13 -13 -30t13 -30q7 -6 13.5 -9t16.5 -3t16.5 3t13.5 9l213 214q13 13 13 30t-13 30v0z" />
+    <glyph glyph-name="uniE934" unicode="&#xe934;" 
+d="M329 439l183 184l183 -184q7 -6 15.5 -9t14.5 -3q7 0 15.5 3t14.5 9q13 13 13 30t-13 30l-213 214q-13 12 -30 12t-30 -12l-213 -214q-13 -13 -13 -30t13 -30q13 -12 30 -12t30 12v0zM542 414q-13 13 -30 13t-30 -13l-213 -213q-13 -13 -13 -30t13 -30t30 -13t30 13
+l183 183l183 -183q7 -7 15.5 -10t14.5 -3q7 0 15.5 3t14.5 10q13 13 13 30t-13 30l-213 213v0z" />
+    <glyph glyph-name="uniE935" unicode="&#xe935;" 
+d="M943 610v3v-3q-29 64 -73 117q-44 52 -99.5 90t-120.5 58q-66 21 -138 21q-58 0 -111 -13q-54 -13 -102 -37.5t-89 -59.5t-73 -78q0 -3 -2.5 -4t-2.5 -4q-41 -58 -65 -127.5t-24 -145.5q0 -90 31 -170q32 -79 87 -141t130 -103q75 -40 161 -51h2.5h2.5q12 0 25 -2.5
+t26 -2.5q97 0 183 37q85 37 149 100.5t100 149.5q37 85 37 183q3 48 -6 95t-28 88zM512 811q50 0 96 -13q47 -12 88 -34.5t76 -53.5q34 -32 60 -70h-320q-67 0 -119 -37t-77 -91l-94 162q25 32 58 57q32 26 69.5 43.5t78.5 26.5q41 10 84 10v0zM640 427q0 -55 -37 -91.5
+t-91 -36.5t-91 36.5t-37 91.5q0 54 37 91t91 37t91 -37t37 -91zM128 427q0 44 9.5 88t28.5 82l158 -277l0.5 -2t4.5 -2q28 -45 76.5 -74t106.5 -29q6 0 12.5 0.5t13.5 4.5l-99 -167q-65 11 -122 45t-99 84t-66 113t-24 134v0zM538 43l157 277v2v2q13 23 19.5 48.5t6.5 54.5
+q0 35 -11.5 68.5t-31.5 59.5h192q10 -29 16 -61t6 -67q1 -77 -26 -145t-75 -119.5t-113 -83.5t-140 -36v0z" />
+    <glyph glyph-name="uniE936" unicode="&#xe936;" 
+d="M512 896q-98 0 -183 -37q-86 -36 -149.5 -100t-100.5 -149q-36 -86 -36 -183q0 -98 36 -183q37 -86 100.5 -149.5t149.5 -100.5q85 -37 183 -37t183 37q86 37 149.5 100.5t100.5 149.5q36 85 36 183q0 97 -36 183q-37 85 -100.5 149t-149.5 100q-85 37 -183 37zM512 43
+q-80 0 -150 30t-122 82t-82 122t-30 150t30 150t82 122t122 82t150 30t150 -30t122 -82t82 -122t30 -150t-30 -150t-82 -122t-122 -82t-150 -30z" />
+    <glyph glyph-name="uniE937" unicode="&#xe937;" 
+d="M768 811h-43q0 35 -25 60t-60 25h-256q-35 0 -60 -25t-25 -60h-43q-54 0 -91 -37t-37 -91v-598q0 -54 37 -91t91 -37h512q54 0 91 37t37 91v598q0 54 -37 91t-91 37zM384 811h256v-86h-256v43v0v0v0v0v43v0zM811 85q0 -19 -12 -30.5t-31 -11.5h-512q-19 0 -31 11.5
+t-12 30.5v598q0 19 12 30.5t31 11.5h43q0 -35 25 -60t60 -25h256q35 0 60 25t25 60h43q19 0 31 -11.5t12 -30.5v-598z" />
+    <glyph glyph-name="uniE938" unicode="&#xe938;" 
+d="M512 896q-98 0 -183 -37q-86 -36 -149.5 -100t-100.5 -149q-36 -86 -36 -183q0 -98 36 -183q37 -86 100.5 -149.5t149.5 -100.5q85 -37 183 -37t183 37q86 37 149.5 100.5t100.5 149.5q36 85 36 183q0 97 -36 183q-37 85 -100.5 149t-149.5 100q-85 37 -183 37zM512 43
+q-80 0 -150 30t-122 82t-82 122t-30 150t30 150t82 122t122 82t150 30t150 -30t122 -82t82 -122t30 -150t-30 -150t-82 -122t-122 -82t-150 -30zM700 380l-145 72v231q0 19 -12 30.5t-31 11.5t-31 -11.5t-12 -30.5v-256q0 -13 6.5 -22.5t19.5 -16.5l171 -85q3 -3 6.5 -3.5
+t10.5 -0.5q12 0 22 6t16 19t0.5 29.5t-21.5 26.5z" />
+    <glyph glyph-name="uniE939" unicode="&#xe939;" 
+d="M768 555h-21q-23 64 -66 116.5t-101 87.5q-67 39 -141.5 50.5t-148.5 -7.5q-77 -22 -137 -68.5t-102 -110.5q-38 -67 -50 -142t8 -148q16 -64 51 -118q35 -53 84.5 -91.5t109.5 -59.5t126 -21v0v0h384q53 0 99 20q47 20 81.5 55t55.5 81q20 47 20 100q0 52 -20 99
+q-19 47 -53 81.5t-80 54.5q-46 21 -99 21v0zM768 128h-384v0v0q-51 0 -98 17q-47 16 -85.5 46t-66.5 70q-27 41 -40 89q-16 61 -7 118.5t41 111.5q32 55 79 89.5t104 47.5q16 3 35 5.5t38 2.5q50 0 96 -16q47 -16 86 -45.5t67 -71.5t41 -93q3 -13 15 -23.5t28 -10.5h51
+q70 0 120.5 -50t50.5 -121q0 -67 -50.5 -116.5t-120.5 -49.5v0z" />
+    <glyph glyph-name="uniE93A" unicode="&#xe93a;" 
+d="M341 171q-19 0 -30.5 -12t-11.5 -31v-85q0 -20 11.5 -31.5t30.5 -11.5q20 0 31.5 11.5t11.5 31.5v85q0 19 -11.5 31t-31.5 12v0zM341 427q-19 0 -30.5 -12t-11.5 -31v-85q0 -20 11.5 -31.5t30.5 -11.5q20 0 31.5 11.5t11.5 31.5v85q0 19 -11.5 31t-31.5 12v0zM683 171
+q-20 0 -31.5 -12t-11.5 -31v-85q0 -20 11.5 -31.5t31.5 -11.5q19 0 30.5 11.5t11.5 31.5v85q0 19 -11.5 31t-30.5 12zM683 427q-20 0 -31.5 -12t-11.5 -31v-85q0 -20 11.5 -31.5t31.5 -11.5q19 0 30.5 11.5t11.5 31.5v85q0 19 -11.5 31t-30.5 12zM512 85q-19 0 -31 -11.5
+t-12 -30.5v-86q0 -19 12 -30.5t31 -11.5t31 11.5t12 30.5v86q0 19 -12 30.5t-31 11.5zM512 341q-19 0 -31 -11.5t-12 -30.5v-86q0 -19 12 -30.5t31 -11.5t31 11.5t12 30.5v86q0 19 -12 30.5t-31 11.5zM1003 529q-32 70 -95 112t-140 42h-21q-24 70 -72 124q-47 54 -108.5 87
+t-133.5 42q-71 9 -143 -10q-78 -19 -139 -66q-61 -46 -98.5 -109.5t-49.5 -139.5q-11 -76 10 -154q16 -61 50 -114t82 -91q13 -10 31.5 -9t28.5 17q9 13 8 31.5t-17 28.5q-38 29 -63.5 71t-38.5 87q-16 59 -7 118q9 58 38.5 108t75.5 86q47 36 106 51q59 16 118 6
+q59 -9 108.5 -38t85.5 -76t51 -106q3 -13 15 -23.5t28 -10.5h55q51 0 93 -27t65 -75q13 -32 15 -66.5t-11 -66.5t-35 -56.5t-54 -36.5q-16 -7 -22 -23.5t0 -32.5q7 -13 16.5 -19.5t22.5 -6.5q3 0 8 1t9 4q48 19 82.5 56t53.5 85q16 51 13.5 101.5t-21.5 98.5v0z" />
+    <glyph glyph-name="uniE93B" unicode="&#xe93b;" 
+d="M1020 478q-10 45 -34 82q-23 38 -57 65t-75 42q-41 16 -86 16v0v0h-21q-24 70 -72 124q-47 54 -108.5 87t-133.5 42q-71 9 -143 -10q-77 -19 -137 -65t-102 -110q-38 -67 -50 -142t8 -148q19 -77 68 -139t119 -100q16 -10 33 -6.5t27 19.5t4.5 33t-21.5 26
+q-54 29 -91.5 78t-53.5 110q-16 58 -7 115t41 111q29 51 77.5 87.5t105.5 49.5q60 16 118 7q59 -9 108.5 -38.5t85.5 -76.5q37 -46 51 -106q3 -12 15 -23t28 -11h51v0v0q61 0 107.5 -37.5t58.5 -98.5q13 -71 -26 -129.5t-106 -71.5q-16 -3 -26.5 -17.5t-7.5 -33.5
+q3 -16 15 -25t28 -9h4h4q51 11 93 41q41 29 68 70.5t38 90.5q11 50 2 101v0zM640 256h-175l128 188q10 16 6.5 33t-19.5 26q-16 10 -33 7t-26 -19l-171 -256q-7 -10 -7 -21.5t7 -21.5q3 -10 12 -15.5t22 -5.5h175l-128 -188q-10 -16 -6.5 -33t19.5 -27q6 -3 12.5 -5.5
+t12.5 -2.5q10 0 19 5t15 12l171 256q7 9 7 21t-7 21q-6 10 -13.5 18t-20.5 8v0z" />
+    <glyph glyph-name="uniE93C" unicode="&#xe93c;" 
+d="M410 725q48 -4 90 -23q43 -18 77 -47.5t59 -67.5q25 -39 38 -84q3 -12 15 -23t28 -11h51v0v0q16 0 34 -3t34 -9q64 -29 91.5 -93.5t-1.5 -128.5q-7 -16 -1 -32.5t22 -23.5q3 -3 8.5 -3.5t8.5 -0.5q13 0 24 6.5t15 19.5q20 48 20 98q0 51 -18.5 97t-53.5 84q-35 37 -85 58
+q-22 6 -47 11.5t-51 5.5h-21q-20 52 -52 97q-33 45 -76 78.5t-94 54.5t-107 26q-19 0 -31.5 -11.5t-15.5 -27.5q-3 -19 10 -31t29 -16v0zM73 926q-13 13 -30 13t-30 -13t-13 -30t13 -30l132 -132q-48 -39 -82.5 -89.5t-53.5 -111.5q-23 -73 -14 -148t48 -142q25 -46 61 -83
+t80 -63t93 -40q50 -14 103 -14h2h2h384q16 0 30 2.5t30 5.5l123 -124q7 -6 15.5 -9t14.5 -3q7 0 15.5 3t14.5 9q13 13 13 30t-13 30l-938 939v0zM384 128q-42 -2 -81 8t-73.5 30t-62.5 50q-28 29 -48 66q-28 51 -34.5 109.5t9.5 116.5q13 51 43 92t72 70l542 -542h-367v0z
+" />
+    <glyph glyph-name="uniE93D" unicode="&#xe93d;" 
+d="M683 427q-20 0 -31.5 -12t-11.5 -31v-341q0 -20 11.5 -31.5t31.5 -11.5q19 0 30.5 11.5t11.5 31.5v341q0 19 -11.5 31t-30.5 12v0zM341 427q-19 0 -30.5 -12t-11.5 -31v-341q0 -20 11.5 -31.5t30.5 -11.5q20 0 31.5 11.5t11.5 31.5v341q0 19 -11.5 31t-31.5 12v0z
+M512 341q-19 0 -31 -11.5t-12 -30.5v-342q0 -19 12 -30.5t31 -11.5t31 11.5t12 30.5v342q0 19 -12 30.5t-31 11.5zM1003 529q-32 70 -95 112t-140 42h-21q-24 70 -72 124q-47 54 -108.5 87t-133.5 42q-71 9 -143 -10q-78 -19 -139 -66q-61 -46 -98.5 -109.5t-49.5 -139.5
+q-11 -76 10 -154q16 -61 50 -114t82 -91q13 -10 31.5 -9t28.5 17q9 13 8 31.5t-17 28.5q-38 29 -63.5 71t-38.5 87q-16 59 -7 118q9 58 38.5 108t75.5 86q47 36 106 51q59 16 118 6q59 -9 108.5 -38t85.5 -76t51 -106q3 -13 15 -23.5t28 -10.5h55q51 0 93 -27t65 -75
+q13 -32 15 -66.5t-11 -66.5t-35 -56.5t-54 -36.5q-16 -7 -22 -23.5t0 -32.5q7 -13 16.5 -19.5t22.5 -6.5q3 0 8 1t9 4q48 19 82.5 56t53.5 85q16 51 13.5 101.5t-21.5 98.5v0z" />
+    <glyph glyph-name="uniE93E" unicode="&#xe93e;" 
+d="M1003 486q-32 71 -95 112.5t-140 41.5h-21q-24 70 -72 124q-47 54 -108.5 87t-133.5 42q-71 9 -143 -10q-78 -19 -139 -65q-61 -47 -98.5 -110.5t-49.5 -139.5q-11 -76 10 -154q16 -61 50 -114t82 -91q13 -10 31.5 -8.5t28.5 17.5q9 12 8 31t-17 28q-38 29 -63.5 71
+t-38.5 87q-16 59 -7 118t38.5 108.5t75.5 85.5q47 36 106 51q59 16 118 7t108.5 -38.5t85.5 -76.5q36 -46 51 -105q3 -13 15 -24t28 -11h55q51 0 93 -27t65 -75q13 -32 15 -66t-11 -66t-35 -56.5t-54 -37.5q-16 -7 -22 -23.5t0 -32.5q7 -12 16.5 -18.5t22.5 -6.5q3 0 8 0.5
+t9 3.5q48 19 82.5 56t53.5 85q16 51 13.5 101.5t-21.5 98.5v0zM311 286q-6 -7 -9 -13.5t-3 -16.5t3 -16.5t9 -13.5q7 -6 14 -9.5t16 -3.5q10 0 17 3.5t13 9.5q7 7 10 13.5t3 16.5t-3 16.5t-10 13.5q-13 13 -30 13t-30 -13v0zM311 115q-6 -6 -9 -13t-3 -17q0 -9 3 -16t9 -14
+q7 -6 14 -9t16 -3q10 0 17 3t13 9q7 7 10 14t3 16q0 10 -3 17t-10 13q-13 13 -30 13t-30 -13v0zM482 201q-6 -7 -9.5 -14t-3.5 -16q0 -10 3.5 -17t9.5 -13q7 -7 13.5 -10t16.5 -3t16.5 3t13.5 10q6 6 9.5 14.5t3.5 15.5q0 6 -3.5 14.5t-9.5 15.5q-13 12 -30 12t-30 -12v0z
+M482 30q-6 -7 -9.5 -13.5t-3.5 -16.5t3.5 -16.5t9.5 -13.5q7 -6 13.5 -9.5t16.5 -3.5t16.5 3.5t13.5 9.5q6 7 9.5 15.5t3.5 14.5t-3.5 14.5t-9.5 15.5q-13 13 -30 13t-30 -13v0zM653 286q-7 -7 -10 -13.5t-3 -16.5t3 -16.5t10 -13.5q6 -6 14.5 -9.5t15.5 -3.5q6 0 14.5 3.5
+t15.5 9.5q6 7 9 15.5t3 14.5t-3 14.5t-9 15.5q-13 13 -30 13t-30 -13v0zM653 115q-7 -6 -10 -13t-3 -17q0 -9 3 -16t10 -14q6 -6 14.5 -9t15.5 -3q6 0 14.5 3t15.5 9q6 7 9 15.5t3 14.5q0 7 -3 15.5t-9 14.5q-13 13 -30 13t-30 -13v0z" />
+    <glyph glyph-name="uniE93F" unicode="&#xe93f;" 
+d="M969 457l-256 256q-13 12 -30 12t-30 -12q-13 -13 -13 -30t13 -30l226 -226l-226 -226q-13 -13 -13 -30t13 -30q6 -7 14.5 -10t15.5 -3q6 0 14.5 3t15.5 10l256 256q12 13 12 30t-12 30v0zM371 713q-13 12 -30 12t-30 -12l-256 -256q-12 -13 -12 -30t12 -30l256 -256
+q7 -7 14 -10t16 -3q10 0 17 3t13 10q13 13 13 30t-13 30l-226 226l226 226q13 13 13 30t-13 30v0z" />
+    <glyph glyph-name="uniE940" unicode="&#xe940;" 
+d="M981 269v4v4v299v4v5v0.5v3.5t-2 6t-2 7v0v0v0v0q-3 3 -4 5.5t-4 2.5v0v0v0l-427 277h-2h-2h-2.5h-2.5q-6 4 -14.5 4t-15.5 -4h-2h-2h-2h-2l-427 -277q-6 0 -10 -2.5t-7 -5.5v0v0v0v0q-3 -7 -4 -10.5t-4 -6.5v-1v-4v-4v-4v-299v-4v-4v-1v-4t2 -6t2 -7v0v0v0v0
+q3 -3 4 -5.5t4 -2.5l427 -277h2h2h2.5h2.5l4.5 -2.5t7.5 -2.5q4 0 7 0.5t6 4.5h2.5h2.5h2h2l426 277q4 3 7 5.5t6 2.5v0v0v0v0q10 10 12 14.5t5 7.5v0v0v0zM128 495l98 -68l-98 -69v137v0zM512 329l-141 98l141 98l141 -98l-141 -98v0zM555 597v180l307 -201l-137 -98
+l-170 119v0zM469 597l-170 -119l-137 98l307 201v-180v0zM299 375l170 -119v-175l-307 201l137 93v0zM555 256l170 119l137 -93l-307 -205v179v0zM798 427l98 68v-137l-98 69v0z" />
+    <glyph glyph-name="uniE941" unicode="&#xe941;" 
+d="M768 341h-85v171h85q70 0 120.5 50t50.5 121q0 70 -50.5 120t-120.5 50t-120.5 -50t-50.5 -120v-86h-170v86q0 70 -50.5 120t-120.5 50t-120.5 -50t-50.5 -120q0 -71 50.5 -121t120.5 -50h85v-171h-85q-70 0 -120.5 -50t-50.5 -120q0 -71 50.5 -121t120.5 -50t120.5 50
+t50.5 121v85h170v-85q0 -71 50.5 -121t120.5 -50t120.5 50t50.5 121q0 70 -50.5 120t-120.5 50zM683 683q0 35 25 60t60 25t60 -25t25 -60q0 -36 -25 -61t-60 -25h-85v86zM341 171q0 -36 -25 -61t-60 -25t-60 25t-25 61q0 35 25 60t60 25h85v-85v0zM341 597h-85
+q-35 0 -60 25t-25 61q0 35 25 60t60 25t60 -25t25 -60v-86v0zM597 341h-170v171h170v-171zM768 85q-35 0 -60 25t-25 61v85h85q35 0 60 -25t25 -60q0 -36 -25 -61t-60 -25z" />
+    <glyph glyph-name="uniE942" unicode="&#xe942;" 
+d="M512 896q-98 0 -183 -37q-86 -36 -149.5 -100t-100.5 -149q-36 -86 -36 -183q0 -98 36 -183q37 -86 100.5 -149.5t149.5 -100.5q85 -37 183 -37t183 37q86 37 149.5 100.5t100.5 149.5q36 85 36 183q0 97 -36 183q-37 85 -100.5 149t-149.5 100q-85 37 -183 37zM512 43
+q-80 0 -150 30t-122 82t-82 122t-30 150t30 150t82 122t122 82t150 30t150 -30t122 -82t82 -122t30 -150t-30 -150t-82 -122t-122 -82t-150 -30zM678 649l-268 -90q-10 -3 -18.5 -11.5t-11.5 -18.5l-90 -273q-3 -13 -0.5 -24.5t9.5 -18.5q6 -6 13 -9t17 -3q3 0 6 0.5t6 3.5
+l273 89q10 4 16.5 10t9.5 16l90 273q3 13 0 24.5t-9 18.5q-6 9 -18 12.5t-25 0.5v0zM567 371l-170 -55l55 170l171 56z" />
+    <glyph glyph-name="uniE943" unicode="&#xe943;" 
+d="M853 597h-384q-54 0 -91 -36.5t-37 -91.5v-384q0 -54 37 -91t91 -37h384q55 0 91.5 37t36.5 91v384q0 55 -36.5 91.5t-91.5 36.5zM896 85q0 -19 -11.5 -30.5t-31.5 -11.5h-384q-19 0 -30.5 11.5t-11.5 30.5v384q0 20 11.5 31.5t30.5 11.5h384q20 0 31.5 -11.5t11.5 -31.5
+v-384zM213 341h-42q-20 0 -31.5 12t-11.5 31v384q0 19 11.5 31t31.5 12h384q19 0 30.5 -12t11.5 -31v-43q0 -19 12 -30.5t31 -11.5t31 11.5t12 30.5v43q0 54 -37 91t-91 37h-384q-55 0 -91.5 -37t-36.5 -91v-384q0 -54 36.5 -91t91.5 -37h42q20 0 31.5 11.5t11.5 31.5
+q0 19 -11.5 30.5t-31.5 11.5v0z" />
+    <glyph glyph-name="uniE944" unicode="&#xe944;" 
+d="M853 811q-19 0 -30.5 -12t-11.5 -31v-299q0 -54 -37 -91t-91 -37h-410l141 141q13 13 13 30t-13 30t-30 13t-30 -13l-213 -213q-3 -4 -6 -7t-3 -6q-3 -7 -3 -16t3 -18q3 -4 4.5 -7t4.5 -6l213 -214q7 -6 13.5 -9t16.5 -3t16.5 3t13.5 9q13 13 13 30t-13 30l-141 141h410
+q44 0 83 17q39 16 68 45t45 68q17 39 17 83v299q0 19 -11.5 31t-31.5 12v0z" />
+    <glyph glyph-name="uniE945" unicode="&#xe945;" 
+d="M892 282q3 6 3 15t-3 19q-3 3 -4.5 6t-4.5 7l-213 213q-13 13 -30 13t-30 -13t-13 -30t13 -30l141 -141h-410q-54 0 -91 37t-37 91v299q0 19 -11.5 31t-30.5 12q-20 0 -31.5 -12t-11.5 -31v-299q0 -44 17 -83q16 -39 45 -68t68 -45q39 -17 83 -17h410l-141 -141
+q-13 -13 -13 -30t13 -30q7 -6 15.5 -9t14.5 -3t14.5 3t15.5 9l213 214l6 6t3 7v0z" />
+    <glyph glyph-name="uniE946" unicode="&#xe946;" 
+d="M853 811h-298q-45 0 -84 -17q-39 -16 -68 -45t-45 -68q-17 -39 -17 -84v-409l-140 141q-13 12 -30 12t-30 -12q-13 -13 -13 -30t13 -30l213 -214q3 -3 6.5 -5.5t6.5 -2.5q3 -3 8.5 -3.5t8.5 -0.5t8.5 0.5t8.5 3.5t6.5 4t6.5 4l213 214q13 13 13 30t-13 30q-13 12 -30 12
+t-30 -12l-140 -141v409q0 55 36.5 91.5t91.5 36.5h298q20 0 31.5 12t11.5 31t-11.5 31t-31.5 12v0z" />
+    <glyph glyph-name="uniE947" unicode="&#xe947;" 
+d="M853 128h-298q-55 0 -91.5 37t-36.5 91v410l140 -141q7 -7 14 -10t16 -3q10 0 17 3t13 10q13 13 13 30t-13 30l-213 213q-3 3 -6.5 5.5t-6.5 2.5q-10 4 -17 4t-17 -4q-3 0 -6.5 -2.5t-6.5 -5.5l-213 -213q-13 -13 -13 -30t13 -30t30 -13t30 13l140 141v-410q0 -45 17 -84
+q16 -39 45 -67.5t68 -45.5q39 -16 84 -16h298q20 0 31.5 11.5t11.5 30.5q0 20 -11.5 31.5t-31.5 11.5v0z" />
+    <glyph glyph-name="uniE948" unicode="&#xe948;" 
+d="M883 329q-13 12 -30 12t-30 -12l-140 -141v409q0 45 -17 84q-16 39 -45 68t-68 45q-39 17 -84 17h-298q-20 0 -31.5 -12t-11.5 -31t11.5 -31t31.5 -12h298q55 0 91.5 -36.5t36.5 -91.5v-409l-140 141q-13 12 -30 12t-30 -12q-13 -13 -13 -30t13 -30l213 -214
+q3 -3 6.5 -5.5t6.5 -2.5q3 -3 8.5 -3.5t8.5 -0.5t8.5 0.5t8.5 3.5t6.5 4t6.5 4l213 214q13 13 13 30t-13 30v0z" />
+    <glyph glyph-name="uniE949" unicode="&#xe949;" 
+d="M883 585l-213 213q-3 3 -6.5 5.5t-6.5 2.5q-6 4 -15 4t-19 -4q-3 -3 -6.5 -4t-6.5 -4l-213 -213q-13 -13 -13 -30t13 -30t30 -13t30 13l140 141v-410q0 -54 -36.5 -91t-91.5 -37h-298q-20 0 -31.5 -11.5t-11.5 -31.5q0 -19 11.5 -30.5t31.5 -11.5h298q45 0 84 16
+q39 17 68 45.5t45 67.5q17 39 17 84v410l140 -141q7 -7 14 -10t16 -3q10 0 17 3t13 10q13 13 13 30t-13 30v0z" />
+    <glyph glyph-name="uniE94A" unicode="&#xe94a;" 
+d="M683 597h-410l141 141q13 13 13 30t-13 30t-30 13t-30 -13l-213 -213q-3 -4 -6 -7t-3 -6q-3 -10 -3 -17.5t3 -16.5q3 -4 4.5 -7t4.5 -6l213 -214q7 -6 13.5 -9t16.5 -3t16.5 3t13.5 9q13 13 13 30t-13 30l-141 141h410q54 0 91 -37t37 -91v-299q0 -19 11.5 -30.5
+t30.5 -11.5q20 0 31.5 11.5t11.5 30.5v299q0 45 -17 84q-16 39 -45 67.5t-68 45.5q-39 16 -83 16v0z" />
+    <glyph glyph-name="uniE94B" unicode="&#xe94b;" 
+d="M892 538q3 6 3 15t-3 19q-3 3 -4.5 6t-4.5 7l-213 213q-13 13 -30 13t-30 -13t-13 -30t13 -30l141 -141h-410q-44 0 -83 -16q-39 -17 -68 -45.5t-45 -67.5q-17 -39 -17 -84v-299q0 -19 11.5 -30.5t31.5 -11.5q19 0 30.5 11.5t11.5 30.5v299q0 54 37 91t91 37h410
+l-141 -141q-13 -13 -13 -30t13 -30q7 -6 15.5 -9t14.5 -3t14.5 3t15.5 9l213 214l6 6t3 7v0z" />
+    <glyph glyph-name="uniE94C" unicode="&#xe94c;" 
+d="M640 597h-256q-19 0 -31 -11.5t-12 -30.5v-256q0 -20 12 -31.5t31 -11.5h256q19 0 31 11.5t12 31.5v256q0 19 -12 30.5t-31 11.5zM597 341h-170v171h170v-171zM981 384h-85v128h85q20 0 31.5 11.5t11.5 31.5q0 19 -11.5 30.5t-31.5 11.5h-85v86q0 54 -37 91t-91 37h-85
+v85q0 19 -12 31t-31 12t-31 -12t-12 -31v-85h-170v85q0 19 -12 31t-31 12t-31 -12t-12 -31v-85h-85q-54 0 -91 -37t-37 -91v-86h-85q-20 0 -31.5 -11.5t-11.5 -30.5q0 -20 11.5 -31.5t31.5 -11.5h85v-128h-85q-20 0 -31.5 -11.5t-11.5 -31.5q0 -19 11.5 -30.5t31.5 -11.5h85
+v-128q0 -55 37 -91.5t91 -36.5h85v-86q0 -19 12 -30.5t31 -11.5t31 11.5t12 30.5v86h170v-86q0 -19 12 -30.5t31 -11.5t31 11.5t12 30.5v86h85q54 0 91 36.5t37 91.5v128h85q20 0 31.5 11.5t11.5 30.5q0 20 -11.5 31.5t-31.5 11.5v0zM811 171q0 -20 -12 -31.5t-31 -11.5
+h-512q-19 0 -31 11.5t-12 31.5v512q0 19 12 30.5t31 11.5h512q19 0 31 -11.5t12 -30.5v-512z" />
+    <glyph glyph-name="uniE94D" unicode="&#xe94d;" 
+d="M896 811h-768q-54 0 -91 -37t-37 -91v-512q0 -55 37 -91.5t91 -36.5h768q54 0 91 36.5t37 91.5v512q0 54 -37 91t-91 37zM128 725h768q19 0 31 -11.5t12 -30.5v-128h-854v128q0 19 12 30.5t31 11.5v0zM896 128h-768q-19 0 -31 11.5t-12 31.5v298h854v-298
+q0 -20 -12 -31.5t-31 -11.5v0z" />
+    <glyph glyph-name="uniE94E" unicode="&#xe94e;" 
+d="M981 213h-170v384q0 55 -37 91.5t-91 36.5l-380 -4v175q0 19 -12 31t-31 12v0v0q-16 0 -29 -12t-13 -31v-175h-175q-20 0 -31.5 -13.5t-11.5 -29.5t13.5 -29t29.5 -13v0v0h175l-5 -380q0 -54 37 -91t91 -37h384v-171q0 -19 12 -30.5t31 -11.5t31 11.5t12 30.5v171h170
+q20 0 31.5 11.5t11.5 31.5q0 19 -11.5 30.5t-31.5 11.5v0zM341 213q-19 0 -30.5 12t-11.5 31l4 380l380 4q19 0 30.5 -11.5t11.5 -31.5v-384h-384v0z" />
+    <glyph glyph-name="uniE94F" unicode="&#xe94f;" 
+d="M512 896q-98 0 -183 -37q-86 -36 -149.5 -100t-100.5 -149q-36 -86 -36 -183q0 -98 36 -183q37 -86 100.5 -149.5t149.5 -100.5q85 -37 183 -37t183 37q86 37 149.5 100.5t100.5 149.5q36 85 36 183q0 97 -36 183q-37 85 -100.5 149t-149.5 100q-85 37 -183 37zM555 47
+v124q0 19 -12 30.5t-31 11.5t-31 -11.5t-12 -30.5v-124q-67 8 -126 37q-58 29 -103 74t-73 103q-28 57 -35 123h124q19 0 31 11.5t12 31.5q0 19 -12 30.5t-31 11.5h-124q7 68 35 126q28 59 73 103.5t103 73.5q59 28 126 34v-123q0 -20 12 -31.5t31 -11.5t31 11.5t12 31.5
+v123q67 -8 126 -37q58 -29 103 -73.5t73 -102.5t35 -124h-124q-19 0 -31 -11.5t-12 -30.5q0 -20 12 -31.5t31 -11.5h124q-7 -67 -35 -126t-73 -103.5t-103 -72.5q-59 -29 -126 -35v0z" />
+    <glyph glyph-name="uniE950" unicode="&#xe950;" 
+d="M512 896q-77 0 -153 -10q-76 -11 -137 -32t-99 -53t-38 -76v-597q0 -43 38 -75t99 -53.5t137 -31.5q76 -11 153 -11t153 11q76 10 137 31.5t99 53.5t38 75v597q0 44 -38 76t-99 53t-137 32q-76 10 -153 10v0zM853 427q0 -8 -20 -23q-20 -14 -62 -28t-107 -24
+q-64 -11 -152 -11t-152 11q-65 10 -107 24t-62 28q-20 15 -20 23v192q30 -16 69 -28t83.5 -20t92.5 -12t96 -4t96 4t92.5 12t83.5 20t69 28v-192zM512 811q83 0 147 -11q64 -10 107 -24t65 -28q22 -15 22 -23t-22 -22t-65 -28t-107 -25q-64 -10 -147 -10t-147 10
+q-64 11 -107 25t-65 28t-22 22t22 23q22 14 65 28t107 24q64 11 147 11zM512 43q-88 0 -152 10q-65 10 -107 24t-62 29q-20 14 -20 22v192q30 -16 69 -28t83.5 -20t92.5 -12t96 -4t96 4t92.5 12t83.5 20t69 28v-192q0 -8 -20 -22q-20 -15 -62 -29t-107 -24q-64 -10 -152 -10
+z" />
+    <glyph glyph-name="uniE951" unicode="&#xe951;" 
+d="M896 811h-555q-9 0 -18 -3.5t-16 -9.5l-298 -341q-10 -13 -10 -28t10 -28l298 -341q7 -10 16 -13.5t18 -3.5h555q54 0 91 36.5t37 91.5v512q0 54 -37 91t-91 37zM939 171q0 -20 -12 -31.5t-31 -11.5h-533l-261 299l261 298h533q19 0 31 -11.5t12 -30.5v-512zM798 585
+q-13 12 -30 12t-30 -12l-98 -99l-98 99q-13 12 -30 12t-30 -12q-13 -13 -13 -30t13 -30l98 -98l-98 -98q-13 -13 -13 -30t13 -30q7 -7 15.5 -10t14.5 -3t14.5 3t15.5 10l98 98l98 -98q7 -7 15.5 -10t14.5 -3t14.5 3t15.5 10q13 13 13 30t-13 30l-98 98l98 98q13 13 13 30
+t-13 30v0z" />
+    <glyph glyph-name="uniE952" unicode="&#xe952;" 
+d="M512 896q-98 0 -183 -37q-86 -36 -149.5 -100t-100.5 -149q-36 -86 -36 -183q0 -98 36 -183q37 -86 100.5 -149.5t149.5 -100.5q85 -37 183 -37t183 37q86 37 149.5 100.5t100.5 149.5q36 85 36 183q0 97 -36 183q-37 85 -100.5 149t-149.5 100q-85 37 -183 37zM512 43
+q-80 0 -150 30t-122 82t-82 122t-30 150t30 150t82 122t122 82t150 30t150 -30t122 -82t82 -122t30 -150t-30 -150t-82 -122t-122 -82t-150 -30zM512 597q-70 0 -120.5 -50t-50.5 -120q0 -71 50.5 -121t120.5 -50t120.5 50t50.5 121q0 70 -50.5 120t-120.5 50zM512 341
+q-35 0 -60 25t-25 61q0 35 25 60t60 25t60 -25t25 -60q0 -36 -25 -61t-60 -25z" />
+    <glyph glyph-name="uniE953" unicode="&#xe953;" 
+d="M619 469h-64v214h170q20 0 31.5 11.5t11.5 30.5q0 20 -11.5 31.5t-31.5 11.5h-170v128q0 19 -12 31t-31 12t-31 -12t-12 -31v-128h-64q-80 0 -136 -56t-56 -136t56 -136t136 -56h64v-213h-213q-19 0 -31 -12t-12 -31t12 -31t31 -12h213v-128q0 -19 12 -30.5t31 -11.5
+t31 11.5t12 30.5v128h64q80 0 136 56t56 136t-56 136t-136 56v0zM405 469q-44 0 -75 31t-31 76t31 76t75 31h64v-214h-64v0zM619 171h-64v213h64q44 0 75 -31t31 -76q0 -44 -31 -75t-75 -31v0z" />
+    <glyph glyph-name="uniE954" unicode="&#xe954;" 
+d="M896 341q-19 0 -31 -11.5t-12 -30.5v-171q0 -19 -11.5 -31t-30.5 -12h-598q-19 0 -30.5 12t-11.5 31v171q0 19 -12 30.5t-31 11.5t-31 -11.5t-12 -30.5v-171q0 -54 37 -91t91 -37h598q54 0 91 37t37 91v171q0 19 -12 30.5t-31 11.5v0zM482 269q3 -3 6.5 -6t6.5 -3
+q3 -3 8.5 -3.5t8.5 -0.5t8.5 0.5t8.5 3.5t6.5 4.5t6.5 4.5l213 213q13 13 13 30t-13 30t-30 13t-30 -13l-140 -141v410q0 19 -12 30.5t-31 11.5t-31 -11.5t-12 -30.5v-410l-140 141q-13 13 -30 13t-30 -13t-13 -30t13 -30l213 -213v0z" />
+    <glyph glyph-name="uniE955" unicode="&#xe955;" 
+d="M653 243l-98 -98v282q0 19 -12 30.5t-31 11.5t-31 -11.5t-12 -30.5v-282l-98 98q-13 13 -30 13t-30 -13q-12 -13 -12 -30t12 -30l171 -170q3 -3 6.5 -6t6.5 -3q3 -3 8.5 -3.5t8.5 -0.5t8.5 0.5t8.5 3.5t6.5 4.5t6.5 4.5l171 170q12 13 12 30t-12 30q-13 13 -30 13
+t-30 -13v0zM977 491q-35 48 -90 77t-119 29v0v0h-21q-24 71 -72 125q-47 54 -108.5 86.5t-133.5 42.5q-71 9 -143 -10q-77 -20 -137 -64t-102 -111q-38 -68 -50 -142.5t8 -148.5q12 -44 32.5 -84.5t52.5 -72.5q13 -13 30 -15t30 10q12 13 14.5 30t-10.5 30
+q-23 26 -40.5 57.5t-23.5 66.5q-16 57 -7 116t41 110t79 87.5t104 48.5q60 16 118 7q59 -9 108.5 -38t85.5 -76q37 -47 51 -106q3 -13 15 -23.5t28 -10.5h51v0v0q42 0 78.5 -18.5t62.5 -54.5q19 -28 27.5 -60t2.5 -68q-7 -35 -25 -63t-44 -47q-16 -10 -18.5 -27t10.5 -33
+q6 -10 15 -13.5t19 -3.5q6 0 12.5 2.5t13.5 5.5q41 29 69 72t37 95q10 51 -6 99t-45 93v0z" />
+    <glyph glyph-name="uniE956" unicode="&#xe956;" 
+d="M785 614l-243 239q-7 7 -13.5 10t-16.5 3v0q-10 0 -16.5 -3t-13.5 -10l-239 -239v0v0q-57 -54 -86 -125t-29 -148q0 -76 28.5 -147t82.5 -126q54 -54 125 -82.5t148 -28.5v0v0q77 0 148 28.5t125 82.5q56 56 84 127t28 145t-28 145q-28 72 -84 129v0zM725 128
+q-44 -42 -98 -63.5t-115 -21.5v0v0q-61 0 -114 21.5t-95 63.5q-45 45 -67.5 99t-22.5 114q0 61 22 114.5t64 94.5v0v0l209 214l213 -214q45 -43 67 -97q23 -55 23.5 -112t-21.5 -113q-21 -55 -65 -100v0z" />
+    <glyph glyph-name="uniE957" unicode="&#xe957;" 
+d="M853 354q-19 0 -30.5 -11.5t-11.5 -31.5v-226q0 -19 -12 -30.5t-31 -11.5h-597q-20 0 -31.5 11.5t-11.5 30.5v598q0 19 11.5 30.5t31.5 11.5h226q19 0 30.5 12t11.5 31t-11.5 31t-30.5 12h-226q-55 0 -91.5 -37t-36.5 -91v-598q0 -54 36.5 -91t91.5 -37h597q54 0 91 37
+t37 91v226q0 20 -11.5 31.5t-31.5 11.5v0zM969 713l-171 170q-13 13 -30 13t-30 -13l-427 -426q-6 -7 -9 -14t-3 -16v-171q0 -19 11.5 -31t30.5 -12h171q10 0 16.5 3.5t13.5 9.5l427 427q12 13 12 30t-12 30v0zM495 299h-111v111l384 384l111 -111z" />
+    <glyph glyph-name="uniE958" unicode="&#xe958;" 
+d="M926 627l-213 214q-13 12 -30 12t-30 -12l-555 -555q-6 -7 -9.5 -13.5t-3.5 -16.5v-213q0 -20 12 -31.5t31 -11.5h213q10 0 17 3t13 10l555 554q13 13 13 30t-13 30v0zM324 85h-153v154l512 512l153 -154l-512 -512v0z" />
+    <glyph glyph-name="uniE959" unicode="&#xe959;" 
+d="M128 171h171q9 0 16 3t14 9l469 470q13 13 13 30t-13 30l-171 170q-13 13 -30 13t-30 -13l-469 -469q-6 -7 -9.5 -13.5t-3.5 -16.5v-171q0 -19 12 -30.5t31 -11.5v0zM171 367l426 427l111 -111l-426 -427h-111v111v0zM896 43h-768q-19 0 -31 -12t-12 -31t12 -31t31 -12
+h768q19 0 31 12t12 31t-12 31t-31 12z" />
+    <glyph glyph-name="uniE95A" unicode="&#xe95a;" 
+d="M768 427q-19 0 -31 -12t-12 -31v-256q0 -19 -11.5 -31t-30.5 -12h-470q-19 0 -30.5 12t-11.5 31v469q0 20 11.5 31.5t30.5 11.5h256q20 0 31.5 11.5t11.5 31.5q0 19 -11.5 30.5t-31.5 11.5h-256q-54 0 -91 -36.5t-37 -91.5v-469q0 -54 37 -91t91 -37h470q54 0 91 37
+t37 91v256q0 19 -12 31t-31 12v0zM934 828q-3 6 -9 12t-12 9q-3 3 -8.5 3.5t-8.5 0.5h-256q-19 0 -31 -11.5t-12 -30.5q0 -20 12 -31.5t31 -11.5h154l-397 -397q-13 -13 -13 -30t13 -30q6 -6 13 -9t17 -3q9 0 16 3t14 9l396 397v-153q0 -20 12 -31.5t31 -11.5t31 11.5
+t12 31.5v256q0 3 -0.5 8.5t-4.5 8.5v0z" />
+    <glyph glyph-name="uniE95B" unicode="&#xe95b;" 
+d="M1020 444q-3 6 -38 64t-99.5 125.5t-157.5 122.5t-213 55t-213 -55t-157.5 -122.5t-99.5 -125.5t-38 -64q-3 -10 -3 -19.5t3 -19.5q3 -4 38 -62q35 -57 99.5 -124t157.5 -121q93 -55 213 -55t213 55t157.5 122t99.5 125t38 65q3 6 3 16.5t-3 17.5v0zM512 128
+q-86 0 -157 37t-124.5 86.5t-89.5 99.5q-35 50 -51 76q14 25 49 75q36 50 90 99.5t125 86.5q72 37 158 37t157 -37t124.5 -86.5t89.5 -99.5q35 -50 51 -75q-16 -26 -51 -76q-36 -50 -89.5 -99.5t-124.5 -86.5t-157 -37v0zM512 597q-70 0 -120.5 -50t-50.5 -120
+q0 -71 50.5 -121t120.5 -50t120.5 50t50.5 121q0 70 -50.5 120t-120.5 50zM512 341q-35 0 -60 25t-25 61q0 35 25 60t60 25t60 -25t25 -60q0 -36 -25 -61t-60 -25z" />
+    <glyph glyph-name="uniE95C" unicode="&#xe95c;" 
+d="M431 717q19 3 40.5 5.5t40.5 2.5q86 0 157 -37t124.5 -86.5t89.5 -99.5q35 -50 51 -75q-16 -29 -35 -57t-41 -50q-13 -13 -11 -31.5t15 -28.5q6 -6 12.5 -7t12.5 -1q10 0 19 3.5t16 13.5q28 32 51.5 69t46.5 76q6 9 6 19t-6 19q-3 3 -38 60q-35 56 -99.5 122.5
+t-157.5 121.5t-213 55q-26 0 -51 -3.5t-47 -9.5q-19 -3 -28 -17.5t-6 -33.5q6 -16 20.5 -24.5t30.5 -5.5v0zM1011 -13l-938 939q-13 13 -30 13t-30 -13t-13 -30t13 -30l183 -183q-57 -52 -105 -110t-87 -125q-6 -10 -6 -19.5t6 -18.5q3 -7 38 -65t99.5 -125t157.5 -122
+t213 -55q67 0 130 19t117 57l192 -192q7 -6 15.5 -9t14.5 -3q7 0 15.5 3t14.5 9q13 13 13 30t-13 30v0zM427 452l106 -106q-6 -4 -12.5 -4.5t-12.5 -0.5q-16 0 -31.5 6t-28.5 16q-13 12 -19.5 27.5t-6.5 31.5q0 7 2.5 14t2.5 16v0zM512 128q-86 0 -157 37t-124.5 86.5
+t-89.5 99.5q-35 50 -51 76q32 57 73.5 105.5t92.5 90.5l107 -111q-13 -22 -19.5 -47.5t-6.5 -50.5q0 -35 15 -66t41 -54q22 -22 52.5 -34.5t62.5 -12.5h2h2q22 0 44 8t41 18l98 -98q-41 -22 -88 -34.5t-95 -12.5v0z" />
+    <glyph glyph-name="uniE95D" unicode="&#xe95d;" 
+d="M768 640q19 0 31 11.5t12 31.5v170q0 20 -12 31.5t-31 11.5h-128q-53 0 -99 -20q-47 -20 -82 -55t-55 -82q-20 -46 -20 -99v-85h-85q-20 0 -31.5 -12t-11.5 -31v-171q0 -19 11.5 -30.5t31.5 -11.5h85v-299q0 -19 11.5 -31t31.5 -12h170q20 0 31.5 12t11.5 31v299h85
+q16 0 28 9t15 25l43 170q3 10 0.5 19.5t-9.5 19.5q-6 9 -15 11t-19 2h-128v85h128v0zM597 469h116l-22 -85h-94q-19 0 -30.5 -11.5t-11.5 -31.5v-298h-86v298q0 20 -11.5 31.5t-30.5 11.5h-86v85h86q19 0 30.5 12t11.5 31v128q0 70 50.5 120.5t120.5 50.5h85v-86h-85
+q-35 0 -60 -25t-25 -60v-128q0 -19 11.5 -31t30.5 -12v0z" />
+    <glyph glyph-name="uniE95E" unicode="&#xe95e;" 
+d="M964 461l-384 298q-9 7 -21.5 9t-25.5 -4q-9 -7 -15 -16.5t-6 -22.5v-597q0 -13 6.5 -22.5t19.5 -15.5q3 -4 8 -4.5t9 -0.5q6 0 12.5 3t12.5 6l384 299q7 6 12 15t5 19q0 9 -3.5 18t-13.5 16v0zM597 213v427l273 -213l-273 -214v0zM111 759q-10 7 -21.5 9t-21.5 -4
+q-13 -7 -19 -16.5t-6 -22.5v-597q0 -13 6 -22.5t19 -15.5q3 -4 8.5 -4.5t8.5 -0.5q7 0 13.5 3t12.5 6l384 299q6 6 11.5 15t5.5 19q0 9 -3.5 18t-13.5 16l-384 298v0zM128 213v427l273 -213l-273 -214v0z" />
+    <glyph glyph-name="uniE95F" unicode="&#xe95f;" 
+d="M892 811q-43 43 -99 64q-55 22 -112.5 22t-112.5 -22q-55 -21 -99 -64l-286 -290q-6 -7 -9 -14t-3 -16v-346l-116 -115q-12 -13 -12 -30t12 -30q7 -6 14 -9.5t16 -3.5q10 0 17 3.5t13 9.5l115 115h346q10 0 16.5 3.5t13.5 9.5l286 290q45 43 67 98t22 112t-22 112
+q-22 56 -67 101v0zM559 171h-243l85 85h243l-85 -85v0zM832 448v0v0l-102 -107h-0.5h-4.5h-239l227 226q12 13 12 30t-12 30q-13 13 -30 13t-30 -13l-299 -298v0v0l-98 -99v244l277 277q32 32 72 48q39 16 80 16t80 -16t71 -48q31 -32 46 -72t14.5 -81t-16.5 -80
+q-16 -40 -48 -70v0z" />
+    <glyph glyph-name="uniE960" unicode="&#xe960;" 
+d="M892 572q-3 3 -4.5 6t-4.5 7l-298 298q-4 3 -7 6t-6 3q-3 3 -8.5 3.5t-8.5 0.5h-299q-54 0 -91 -37t-37 -91v-683q0 -54 37 -91t91 -37h512q54 0 91 37t37 91v470q0 3 -0.5 8.5t-3.5 8.5v0zM597 751l154 -154h-154v154zM768 43h-512q-19 0 -31 11.5t-12 30.5v683
+q0 19 12 31t31 12h256v-256q0 -20 11.5 -31.5t31.5 -11.5h256v-427q0 -19 -12 -30.5t-31 -11.5v0z" />
+    <glyph glyph-name="uniE961" unicode="&#xe961;" 
+d="M892 614q-3 4 -4.5 7t-4.5 6l-256 256l-6 6t-7 3q-3 3 -8 3.5t-9 0.5h-341q-54 0 -91 -37t-37 -91v-683q0 -54 37 -91t91 -37h512q54 0 91 37t37 91v512q0 4 -0.5 9t-3.5 8zM640 751l111 -111h-111v111zM768 43h-512q-19 0 -31 11.5t-12 30.5v683q0 19 12 31t31 12h299
+v-214q0 -19 11.5 -30.5t30.5 -11.5h214v-470q0 -19 -12 -30.5t-31 -11.5v0zM640 341h-256q-19 0 -31 -11.5t-12 -30.5q0 -20 12 -31.5t31 -11.5h256q19 0 31 11.5t12 31.5q0 19 -12 30.5t-31 11.5z" />
+    <glyph glyph-name="uniE962" unicode="&#xe962;" 
+d="M892 614q-3 4 -4.5 7t-4.5 6l-256 256l-6 6t-7 3q-3 3 -8 3.5t-9 0.5h-341q-54 0 -91 -37t-37 -91v-683q0 -54 37 -91t91 -37h512q54 0 91 37t37 91v512q0 4 -0.5 9t-3.5 8zM640 751l111 -111h-111v111zM768 43h-512q-19 0 -31 11.5t-12 30.5v683q0 19 12 31t31 12h299
+v-214q0 -19 11.5 -30.5t30.5 -11.5h214v-470q0 -19 -12 -30.5t-31 -11.5v0zM640 341h-85v86q0 19 -12 30.5t-31 11.5t-31 -11.5t-12 -30.5v-86h-85q-19 0 -31 -11.5t-12 -30.5q0 -20 12 -31.5t31 -11.5h85v-85q0 -20 12 -31.5t31 -11.5t31 11.5t12 31.5v85h85q19 0 31 11.5
+t12 31.5q0 19 -12 30.5t-31 11.5z" />
+    <glyph glyph-name="uniE963" unicode="&#xe963;" 
+d="M892 614q-3 4 -4.5 7t-4.5 6l-256 256l-6 6t-7 3q-3 3 -8 3.5t-9 0.5h-341q-54 0 -91 -37t-37 -91v-683q0 -54 37 -91t91 -37h512q54 0 91 37t37 91v512q0 4 -0.5 9t-3.5 8zM640 751l111 -111h-111v111zM768 43h-512q-19 0 -31 11.5t-12 30.5v683q0 19 12 31t31 12h299
+v-214q0 -19 11.5 -30.5t30.5 -11.5h214v-470q0 -19 -12 -30.5t-31 -11.5v0zM683 427h-342q-19 0 -30.5 -12t-11.5 -31t11.5 -31t30.5 -12h342q19 0 30.5 12t11.5 31t-11.5 31t-30.5 12v0zM683 256h-342q-19 0 -30.5 -11.5t-11.5 -31.5q0 -19 11.5 -30.5t30.5 -11.5h342
+q19 0 30.5 11.5t11.5 30.5q0 20 -11.5 31.5t-30.5 11.5v0zM341 512h86q19 0 30.5 11.5t11.5 31.5q0 19 -11.5 30.5t-30.5 11.5h-86q-19 0 -30.5 -11.5t-11.5 -30.5q0 -20 11.5 -31.5t30.5 -11.5v0z" />
+    <glyph glyph-name="uniE964" unicode="&#xe964;" 
+d="M845 896h-666q-57 0 -96.5 -39.5t-39.5 -97.5v-665q0 -58 39.5 -97.5t96.5 -39.5h666q57 0 96.5 39.5t39.5 97.5v665q0 58 -39.5 97.5t-96.5 39.5zM768 597h128v-128h-128v128zM683 469h-342v342h342v-342zM256 469h-128v128h128v-128zM128 384h128v-128h-128v128z
+M341 384h342v-341h-342v341v0zM768 384h128v-128h-128v128zM896 759v-76h-128v128h77q22 0 36.5 -14.5t14.5 -37.5zM179 811h77v-128h-128v76q0 23 14.5 37.5t36.5 14.5zM128 94v77h128v-128h-77q-22 0 -36.5 14t-14.5 37v0zM845 43h-77v128h128v-77q0 -23 -14.5 -37
+t-36.5 -14z" />
+    <glyph glyph-name="uniE965" unicode="&#xe965;" 
+d="M977 828q-6 13 -16 19t-22 6h-854q-12 0 -22 -6t-16 -19q-6 -10 -4 -21.5t8 -21.5l333 -392v-265q0 -13 6.5 -22.5t19.5 -15.5l170 -86q3 -3 7 -3.5t10 -0.5q7 0 12.5 0.5t9.5 3.5q9 7 15 16.5t6 22.5v350l333 392q6 10 8 21.5t-4 21.5v0zM563 435q-3 -6 -5.5 -13
+t-2.5 -17v-294l-86 43v251q0 7 -2.5 15t-5.5 11l-282 337h670l-286 -333v0z" />
+    <glyph glyph-name="uniE966" unicode="&#xe966;" 
+d="M870 849q-12 6 -24.5 3.5t-22.5 -11.5l-32 -15t-108 -15q-42 0 -79 12.5t-75 25.5q-42 19 -87.5 33t-100.5 14q-102 0 -148 -24.5t-52 -30.5q-7 -7 -10 -14t-3 -16v-811q0 -19 11.5 -31t31.5 -12q19 0 30.5 12t11.5 31v277q13 7 45 14.5t83 7.5q42 0 79 -13t75 -26
+q42 -16 87.5 -31.5t100.5 -15.5q102 0 148 24.5t52 31.5q7 6 10 13t3 17v512q0 12 -6.5 22t-19.5 16v0zM811 320q-13 -6 -45 -13.5t-83 -7.5q-42 0 -79 12.5t-75 25.5q-42 19 -87.5 33t-100.5 14q-41 0 -73 -5.5t-55 -11.5v422q13 7 45 14.5t83 7.5q42 0 79 -13t75 -26
+q42 -19 87.5 -33t100.5 -14q41 0 73 5.5t55 11.5v-422v0z" />
+    <glyph glyph-name="uniE967" unicode="&#xe967;" 
+d="M853 725h-362l-73 111q-6 7 -15 12t-19 5h-213q-55 0 -91.5 -36.5t-36.5 -91.5v-597q0 -54 36.5 -91t91.5 -37h682q55 0 91.5 37t36.5 91v469q0 55 -36.5 91.5t-91.5 36.5v0zM896 128q0 -19 -11.5 -31t-31.5 -12h-682q-20 0 -31.5 12t-11.5 31v597q0 20 11.5 31.5
+t31.5 11.5h192l72 -111q7 -6 16 -11.5t18 -5.5h384q20 0 31.5 -11.5t11.5 -31.5v-469v0z" />
+    <glyph glyph-name="uniE968" unicode="&#xe968;" 
+d="M853 725h-362l-73 111q-6 7 -15 12t-19 5h-213q-55 0 -91.5 -36.5t-36.5 -91.5v-597q0 -54 36.5 -91t91.5 -37h682q55 0 91.5 37t36.5 91v469q0 55 -36.5 91.5t-91.5 36.5v0zM896 128q0 -19 -11.5 -31t-31.5 -12h-682q-20 0 -31.5 12t-11.5 31v597q0 20 11.5 31.5
+t31.5 11.5h192l72 -111q7 -6 16 -11.5t18 -5.5h384q20 0 31.5 -11.5t11.5 -31.5v-469v0zM640 384h-256q-19 0 -31 -11.5t-12 -31.5q0 -19 12 -30.5t31 -11.5h256q19 0 31 11.5t12 30.5q0 20 -12 31.5t-31 11.5z" />
+    <glyph glyph-name="uniE969" unicode="&#xe969;" 
+d="M853 725h-362l-73 111q-6 7 -15 12t-19 5h-213q-55 0 -91.5 -36.5t-36.5 -91.5v-597q0 -54 36.5 -91t91.5 -37h682q55 0 91.5 37t36.5 91v469q0 55 -36.5 91.5t-91.5 36.5v0zM896 128q0 -19 -11.5 -31t-31.5 -12h-682q-20 0 -31.5 12t-11.5 31v597q0 20 11.5 31.5
+t31.5 11.5h192l72 -111q7 -6 16 -11.5t18 -5.5h384q20 0 31.5 -11.5t11.5 -31.5v-469v0zM640 384h-85v85q0 20 -12 31.5t-31 11.5t-31 -11.5t-12 -31.5v-85h-85q-19 0 -31 -11.5t-12 -31.5q0 -19 12 -30.5t31 -11.5h85v-86q0 -19 12 -30.5t31 -11.5t31 11.5t12 30.5v86h85
+q19 0 31 11.5t12 30.5q0 20 -12 31.5t-31 11.5z" />
+    <glyph glyph-name="uniE96A" unicode="&#xe96a;" 
+d="M939 683q0 70 -50.5 120t-120.5 50t-120.5 -50t-50.5 -120q0 -58 37 -104t91 -58q-6 -60 -32 -112t-66.5 -92.5t-92.5 -66.5t-112 -32q-12 44 -44.5 76.5t-74.5 42.5v474q0 19 -12 30.5t-31 11.5t-33 -11.5t-14 -30.5v-474q-54 -13 -91 -59.5t-37 -106.5q0 -71 50.5 -121
+t120.5 -50q61 0 105.5 37t60.5 91q77 6 145 39t120 85t85 120q32 67 39 144q54 16 91 61t37 106v0zM256 85q-35 0 -60 25t-25 61q0 35 25 60t60 25t60 -25t25 -60q0 -36 -25 -61t-60 -25v0zM768 597q-35 0 -60 25t-25 61q0 35 25 60t60 25t60 -25t25 -60q0 -36 -25 -61
+t-60 -25z" />
+    <glyph glyph-name="uniE96B" unicode="&#xe96b;" 
+d="M981 469h-256h-2h-2q-16 74 -74 122.5t-135 48.5t-135 -48.5t-74 -122.5h-2h-2h-256q-20 0 -31.5 -11.5t-11.5 -30.5q0 -20 11.5 -31.5t31.5 -11.5h256h2h2q16 -74 74 -122.5t135 -48.5t135 48.5t74 122.5h2h2h256q20 0 31.5 11.5t11.5 31.5q0 19 -13.5 30.5t-29.5 11.5
+v0zM512 299q-54 0 -91 36.5t-37 91.5q0 54 37 91t91 37t91 -37t37 -91q0 -55 -37 -91.5t-91 -36.5z" />
+    <glyph glyph-name="uniE96C" unicode="&#xe96c;" 
+d="M768 341q-58 0 -103.5 -36.5t-58.5 -91.5q-59 7 -112 33q-52 26 -92.5 66t-66.5 93q-26 52 -32 111q54 16 91 60.5t37 101.5q0 71 -50 121t-121 50q-70 0 -122.5 -48t-52.5 -118q0 -61 37 -106t91 -61v-473q0 -20 12 -31.5t31 -11.5t31 11.5t12 31.5v256q27 -36 60 -65
+q34 -29 72 -51t81 -36t90 -19q12 -54 58.5 -93t107.5 -39q70 0 120.5 50t50.5 120q0 71 -50.5 123t-120.5 52v0zM171 683q0 35 25 60t60 25t60 -25t25 -60q0 -36 -25 -61t-60 -25t-60 25t-25 61v0zM768 85q-35 0 -60 25t-25 61q0 35 25 60t60 25t60 -25t25 -60
+q0 -36 -25 -61t-60 -25z" />
+    <glyph glyph-name="uniE96D" unicode="&#xe96d;" 
+d="M811 337v260q0 55 -37 91.5t-91 36.5h-128q-20 0 -31.5 -11.5t-11.5 -30.5q0 -20 11.5 -31.5t31.5 -11.5h128q19 0 30.5 -11.5t11.5 -31.5v-260q-54 -13 -91 -59.5t-37 -106.5q0 -71 50.5 -121t120.5 -50t120.5 50t50.5 121q0 60 -37 105t-91 61v0zM768 85q-35 0 -60 25
+t-25 61q0 35 25 60t60 25t60 -25t25 -60q0 -36 -25 -61t-60 -25zM256 853q-70 0 -120.5 -50t-50.5 -120q0 -61 37 -106t91 -61v-473q0 -20 12 -31.5t31 -11.5t31 11.5t12 31.5v473q54 13 91 59.5t37 107.5q0 70 -50.5 120t-120.5 50zM256 597q-35 0 -60 25t-25 61
+q0 35 25 60t60 25t60 -25t25 -60q0 -36 -25 -61t-60 -25v0z" />
+    <glyph glyph-name="uniE96E" unicode="&#xe96e;" 
+d="M960 576q0 48 -15.5 90t-44.5 81q10 41 6.5 84.5t-19.5 81.5q-3 10 -9.5 16t-15.5 10q-13 3 -57 -1.5t-131 -58.5q-70 16 -142 16t-139 -16q-87 54 -131 58.5t-57 1.5q-10 -4 -16.5 -10t-9.5 -16q-19 -42 -22 -83.5t9 -82.5q-28 -39 -43.5 -82.5t-15.5 -88.5
+q0 -86 22 -145q21 -58 56.5 -96t79.5 -58t89 -30q-6 -22 -9.5 -41.5t-3.5 -39.5v-4q-67 -13 -97.5 8t-55.5 56q-16 23 -37.5 44.5t-56.5 28.5q-16 3 -32 -5.5t-19 -24.5q-4 -16 5 -32t25 -19q9 -4 21.5 -15.5t24.5 -27.5q29 -38 77.5 -76.5t144.5 -25.5v-73q0 -19 12 -31
+t31 -12t31 12t12 31v124v2v2v38q0 23 7 42t23 35q9 10 11.5 21.5t-3.5 21.5q-3 13 -12 20t-22 10q-46 6 -90 19q-43 12 -76 40t-53 75t-20 122q0 38 13 70.5t38 61.5q10 10 11 21.5t-2 21.5q-10 28 -10.5 54.5t5.5 51.5q16 -3 44.5 -14.5t66.5 -40.5q10 -7 19.5 -7.5
+t19.5 3.5q67 19 138.5 19t138.5 -19q10 -4 19 -1.5t15 5.5q42 29 70 40.5t41 14.5q7 -25 6.5 -50.5t-10.5 -51.5q-3 -13 -2 -24.5t10 -18.5q26 -25 39 -59.5t13 -72.5q0 -75 -20 -123q-20 -47 -53.5 -75t-76.5 -41q-43 -12 -89 -17q-13 0 -22 -8.5t-12 -21.5
+q-4 -13 -1 -24.5t9 -18.5q16 -16 23 -37t7 -44v-166q0 -19 11.5 -31t31.5 -12q19 0 30.5 12t11.5 31v162q4 23 0.5 42.5t-12.5 38.5q38 8 81 27q44 19 80.5 56.5t61.5 97.5q24 61 24 152v0z" />
+    <glyph glyph-name="uniE96F" unicode="&#xe96f;" 
+d="M1020 380l-158 478q-3 6 -7 12.5t-10 12.5q-16 16 -40.5 16t-40.5 -16q-7 -3 -10.5 -9.5t-6.5 -15.5l-94 -291h-282l-94 291q-3 6 -6.5 12.5t-10.5 12.5q-16 16 -40.5 16t-40.5 -16q-6 -3 -10 -9.5t-7 -15.5l-158 -478q-9 -26 -0.5 -50t30.5 -40l452 -328q7 -4 13.5 -6.5
+t12.5 -2.5t12.5 2.5t13.5 6.5l452 328q22 13 30.5 38.5t-0.5 51.5v0zM512 47l-427 307l133 405l81 -247q3 -13 14.5 -21.5t27.5 -8.5h342q12 0 24 8.5t18 21.5l81 247l81 -247l52 -154l-427 -311v0z" />
+    <glyph glyph-name="uniE970" unicode="&#xe970;" 
+d="M512 896q-98 0 -183 -37q-86 -36 -149.5 -100t-100.5 -149q-36 -86 -36 -183q0 -98 36 -183q37 -86 100.5 -149.5t149.5 -100.5q85 -37 183 -37t183 37q86 37 149.5 100.5t100.5 149.5q36 85 36 183q0 97 -36 183q-37 85 -100.5 149t-149.5 100q-85 37 -183 37zM892 469
+h-171q-3 45 -12 88t-23.5 84.5t-34.5 80.5t-45 76q57 -15 107 -45q49 -31 87 -74.5t61 -96.5q24 -53 31 -113zM388 384h252q-5 -43 -15 -85t-26 -82t-36 -78q-21 -37 -47 -71q-29 34 -51 71q-22 38 -38 78t-26 82t-13 85v0zM388 469q5 44 15 86q11 42 26.5 82t36.5 77
+q20 37 46 71q27 -35 49 -73q21 -38 36.5 -78t25.5 -82q10 -41 13 -83h-248v0zM414 798q-24 -37 -43 -76t-33 -80.5t-23 -84.5t-12 -88h-171q7 60 30 113q24 53 61.5 96.5t85.5 74.5q49 30 105 45v0zM132 384h171q3 -45 12 -88t23.5 -84t34.5 -81q20 -39 45 -76
+q-57 15 -107 46q-49 31 -87 74t-61 96q-24 54 -31 113v0zM610 55q24 37 43 76q20 40 34 81t24 84t14 88h171q-8 -59 -33 -113q-24 -53 -62 -96t-86 -74q-49 -31 -105 -46z" />
+    <glyph glyph-name="uniE971" unicode="&#xe971;" 
+d="M427 853h-299q-19 0 -31 -11.5t-12 -30.5v-299q0 -19 12 -31t31 -12h299q19 0 30.5 12t11.5 31v299q0 19 -11.5 30.5t-30.5 11.5v0zM384 555h-213v213h213v-213zM896 853h-299q-19 0 -30.5 -11.5t-11.5 -30.5v-299q0 -19 11.5 -31t30.5 -12h299q19 0 31 12t12 31v299
+q0 19 -12 30.5t-31 11.5v0zM853 555h-213v213h213v-213zM896 384h-299q-19 0 -30.5 -11.5t-11.5 -31.5v-298q0 -20 11.5 -31.5t30.5 -11.5h299q19 0 31 11.5t12 31.5v298q0 20 -12 31.5t-31 11.5zM853 85h-213v214h213v-214zM427 384h-299q-19 0 -31 -11.5t-12 -31.5v-298
+q0 -20 12 -31.5t31 -11.5h299q19 0 30.5 11.5t11.5 31.5v298q0 20 -11.5 31.5t-30.5 11.5v0zM384 85h-213v214h213v-214z" />
+    <glyph glyph-name="uniE972" unicode="&#xe972;" 
+d="M977 444v0v0l-149 294q-16 32 -46.5 52.5t-68.5 20.5h-406q-35 0 -65 -19t-46 -54l-145 -294v0v0q-6 -3 -7 -7t-1 -10v-256q0 -55 36.5 -91.5t91.5 -36.5h682q55 0 91.5 36.5t36.5 91.5v256q0 6 -0.5 10t-3.5 7v0zM269 700v0v0q6 13 17.5 19t20.5 6h406q12 0 22 -6
+t16 -19l115 -231h-712zM853 128h-682q-20 0 -31.5 11.5t-11.5 31.5v213h768v-213q0 -20 -11.5 -31.5t-31.5 -11.5v0zM226 286q-6 -7 -9.5 -13.5t-3.5 -16.5t3.5 -16.5t9.5 -13.5q7 -6 13.5 -9.5t16.5 -3.5t16.5 3.5t13.5 9.5q6 7 9.5 13.5t3.5 16.5t-3.5 16.5t-9.5 13.5
+q-13 13 -30 13t-30 -13v0zM397 286q-7 -7 -10 -13.5t-3 -16.5t3 -16.5t10 -13.5q6 -6 13 -9.5t17 -3.5q9 0 16 3.5t14 9.5q6 7 9 15.5t3 14.5t-3 14.5t-9 15.5q-13 13 -30 13t-30 -13v0z" />
+    <glyph glyph-name="uniE973" unicode="&#xe973;" 
+d="M853 341h-179l17 171h162q20 0 31.5 11.5t11.5 31.5q0 19 -11.5 30.5t-31.5 11.5h-149l21 209q4 16 -7.5 30t-30.5 17q-16 4 -30 -7.5t-17 -30.5l-26 -218h-170l25 209q4 16 -7.5 30t-30.5 17q-16 4 -30 -7.5t-17 -30.5l-26 -218h-187q-20 0 -31.5 -11.5t-11.5 -30.5
+q0 -20 11.5 -31.5t31.5 -11.5h179l-17 -171h-162q-20 0 -31.5 -11.5t-11.5 -30.5q0 -20 11.5 -31.5t31.5 -11.5h149l-21 -209q-4 -16 7.5 -30t30.5 -17h2h2q16 0 28 11t15 27l26 218h170l-25 -209q-4 -16 7.5 -30t30.5 -17h2h2q16 0 28 11t15 27l26 218h187q20 0 31.5 11.5
+t11.5 31.5q0 19 -11.5 30.5t-31.5 11.5v0zM418 341l17 171h171l-17 -171h-171v0z" />
+    <glyph glyph-name="uniE974" unicode="&#xe974;" 
+d="M512 853q-88 0 -166 -33q-77 -34 -135 -92t-92 -136q-34 -77 -34 -165v-299q0 -54 37 -91t91 -37h43q54 0 91 37t37 91v128q0 54 -37 91t-91 37h-85v43q0 70 27 132t73 108.5t108 73.5q63 27 133 27t133 -27q62 -27 108 -73.5t73 -108.5t27 -132v-43h-85q-54 0 -91 -37
+t-37 -91v-128q0 -54 37 -91t91 -37h43q54 0 91 37t37 91v299q0 88 -34 165q-34 78 -92 136t-135 92q-78 33 -166 33v0zM256 299q19 0 31 -12t12 -31v-128q0 -19 -12 -31t-31 -12h-43q-19 0 -30.5 12t-11.5 31v171h85v0zM853 128q0 -19 -11.5 -31t-30.5 -12h-43q-19 0 -31 12
+t-12 31v128q0 19 12 31t31 12h85v-171v0z" />
+    <glyph glyph-name="uniE975" unicode="&#xe975;" 
+d="M917 772v0q-38 39 -87.5 60t-104.5 21v0q-54 0 -106 -21t-90 -60v0v0l-17 -17l-17 17q-38 39 -88.5 60t-107.5 21q-55 0 -104.5 -21t-87.5 -60q-39 -38 -62.5 -90t-23.5 -106t21.5 -106t59.5 -90l376 -376q6 -6 13 -9.5t17 -3.5q9 0 16 3.5t14 9.5l375 376q43 40 65 91
+t22 105t-20 105q-21 51 -63 91v0zM858 439l-346 -345l-346 345q-57 58 -57 137t57 137q26 28 61.5 41.5t71.5 13.5q38 0 72.5 -13.5t63.5 -41.5l47 -47q13 -13 30 -13t30 13l43 47v0v0q28 25 63.5 40t76.5 15v0v0q39 0 73.5 -15t63.5 -40v0q25 -29 40 -64t15 -73t-15.5 -73
+t-43.5 -64v0z" />
+    <glyph glyph-name="uniE976" unicode="&#xe976;" 
+d="M563 674q-67 23 -131 -7.5t-86 -94.5q-4 -19 3 -36t26 -20q16 -6 33 2t23 24q13 32 44.5 48t66.5 3q25 -10 40 -31t15 -50q0 -32 -39.5 -56t-58.5 -34q-16 -6 -24 -21t-1 -34q3 -13 14 -21.5t24 -8.5q3 0 6.5 0.5t6.5 3.5q6 2 32 13q25 10 53 30.5t50 51.5q23 30 23 72
+q-4 57 -36.5 102t-83.5 64v0zM512 896q-98 0 -183 -37q-86 -36 -149.5 -100t-100.5 -149q-36 -86 -36 -183q0 -98 36 -183q37 -86 100.5 -149.5t149.5 -100.5q85 -37 183 -37t183 37q86 37 149.5 100.5t100.5 149.5q36 85 36 183q0 97 -36 183q-37 85 -100.5 149t-149.5 100
+q-85 37 -183 37zM512 43q-80 0 -150 30t-122 82t-82 122t-30 150t30 150t82 122t122 82t150 30t150 -30t122 -82t82 -122t30 -150t-30 -150t-82 -122t-122 -82t-150 -30zM482 243q-6 -6 -9.5 -13t-3.5 -17q0 -9 3.5 -16t9.5 -14q7 -6 13.5 -9t16.5 -3t16.5 3t13.5 9
+q6 7 9.5 15.5t3.5 14.5q0 7 -3.5 15.5t-9.5 14.5q-13 13 -30 13t-30 -13v0z" />
+    <glyph glyph-name="uniE977" unicode="&#xe977;" 
+d="M922 589l-384 298q-13 10 -27.5 10t-24.5 -10l-384 -298q-9 -7 -13 -16t-4 -18v-470q0 -54 37 -91t91 -37h598q54 0 91 37t37 91v470q0 9 -4 18t-13 16v0zM597 43h-170v341h170v-341zM853 85q0 -19 -11.5 -30.5t-30.5 -11.5h-128v384q0 19 -12 30.5t-31 11.5h-256
+q-19 0 -31 -11.5t-12 -30.5v-384h-128q-19 0 -30.5 11.5t-11.5 30.5v448l341 265l341 -265v-448v0z" />
+    <glyph glyph-name="uniE978" unicode="&#xe978;" 
+d="M811 853h-598q-54 0 -91 -36.5t-37 -91.5v-597q0 -54 37 -91t91 -37h598q54 0 91 37t37 91v597q0 55 -37 91.5t-91 36.5zM171 725q0 20 11.5 31.5t30.5 11.5h598q19 0 30.5 -11.5t11.5 -31.5v-324l-140 141q-13 13 -30 13t-30 -13l-452 -452q-13 3 -21.5 14t-8.5 24v597
+v0zM811 85h-495l367 367l170 -170v-154q0 -19 -11.5 -31t-30.5 -12v0zM363 469q44 0 75 31t31 76t-31 76t-75 31q-45 0 -76 -31t-31 -76t31 -76t76 -31v0zM363 597q9 0 15 -5.5t6 -15.5t-6 -15.5t-15 -5.5q-10 0 -16 5.5t-6 15.5t6 15.5t16 5.5v0z" />
+    <glyph glyph-name="uniE979" unicode="&#xe979;" 
+d="M977 444v0v0l-149 294q-16 32 -46.5 52.5t-68.5 20.5h-406q-35 0 -65 -19t-46 -54l-145 -294v0v0q-6 -3 -7 -7t-1 -10v-256q0 -55 36.5 -91.5t91.5 -36.5h682q55 0 91.5 36.5t36.5 91.5v256q0 6 -0.5 10t-3.5 7v0zM269 700v0v0q6 13 17.5 19t20.5 6h406q12 0 22 -6
+t16 -19l115 -231h-183q-10 0 -19 -5t-15 -12l-73 -111h-124l-72 111q-10 7 -19.5 12t-19.5 5h-187l115 231v0zM853 128h-682q-20 0 -31.5 11.5t-11.5 31.5v213h192l73 -111q6 -6 15 -11.5t19 -5.5h170q10 0 19 5.5t15 11.5l73 111h192v-213q0 -20 -11.5 -31.5t-31.5 -11.5v0
+z" />
+    <glyph glyph-name="uniE97A" unicode="&#xe97a;" 
+d="M512 896q-98 0 -183 -37q-86 -36 -149.5 -100t-100.5 -149q-36 -86 -36 -183q0 -98 36 -183q37 -86 100.5 -149.5t149.5 -100.5q85 -37 183 -37t183 37q86 37 149.5 100.5t100.5 149.5q36 85 36 183q0 97 -36 183q-37 85 -100.5 149t-149.5 100q-85 37 -183 37zM512 43
+q-80 0 -150 30t-122 82t-82 122t-30 150t30 150t82 122t122 82t150 30t150 -30t122 -82t82 -122t30 -150t-30 -150t-82 -122t-122 -82t-150 -30zM512 469q-19 0 -31 -11.5t-12 -30.5v-171q0 -19 12 -31t31 -12t31 12t12 31v171q0 19 -12 30.5t-31 11.5v0zM482 627
+q-6 -6 -9.5 -13t-3.5 -17q0 -9 3.5 -16t9.5 -14q7 -6 13.5 -9t16.5 -3t16.5 3t13.5 9q6 7 9.5 14t3.5 16q0 10 -3.5 17t-9.5 13q-13 13 -30 13t-30 -13v0z" />
+    <glyph glyph-name="uniE97B" unicode="&#xe97b;" 
+d="M725 896h-426q-53 0 -100 -20q-46 -20 -81 -55t-55 -82q-20 -46 -20 -99v-427q0 -52 20 -99t55 -81.5t81 -54.5q47 -21 100 -21h426q53 0 100 21q46 20 81 54.5t55 81.5t20 99v427q0 53 -20 99q-20 47 -55 82t-81 55q-47 20 -100 20v0zM896 213q0 -70 -50 -120t-121 -50
+h-426q-71 0 -121 50t-50 120v427q0 70 50 120.5t121 50.5h426q71 0 121 -50.5t50 -120.5v-427zM546 640q-16 3 -32 3t-32 -3q-43 -6 -79 -29t-60 -56t-35 -74t-5 -84q6 -42 28 -78.5t57 -62.5q29 -19 61 -31t67 -12q7 0 15.5 0.5t14.5 4.5q42 6 78.5 28t62.5 57t35.5 75.5
+t2.5 82.5q-9 70 -58.5 119.5t-120.5 59.5zM619 354q-16 -19 -38 -33.5t-48 -17.5q-51 -6 -94.5 24.5t-50.5 82.5q-9 51 23.5 94.5t83.5 50.5h8.5h8.5h8.5h8.5q42 -7 71 -36t36 -71q6 -26 0.5 -50.5t-17.5 -43.5v0zM717 691q-7 -6 -10 -13t-3 -17q0 -9 3 -16t10 -14
+q6 -6 14.5 -9t15.5 -3q9 0 16 3t14 9q6 7 9 15.5t3 14.5q0 10 -3 17t-9 13q-13 13 -30 13t-30 -13v0z" />
+    <glyph glyph-name="uniE97C" unicode="&#xe97c;" 
+d="M811 811h-384q-20 0 -31.5 -12t-11.5 -31t11.5 -31t31.5 -12h153l-226 -597h-141q-19 0 -30.5 -11.5t-11.5 -31.5q0 -19 11.5 -30.5t30.5 -11.5h384q20 0 31.5 11.5t11.5 30.5q0 20 -11.5 31.5t-31.5 11.5h-153l222 597h145q19 0 30.5 12t11.5 31t-11.5 31t-30.5 12v0z
+" />
+    <glyph glyph-name="uniE97D" unicode="&#xe97d;" 
+d="M68 602l427 -214q3 -3 7 -3.5t10 -0.5t10 0.5t7 3.5l427 214q13 6 19 15.5t6 22.5t-6 22.5t-19 15.5l-427 214q-10 3 -19.5 3t-18.5 -3l-427 -214q-10 -6 -15.5 -15.5t-5.5 -22.5t6 -22.5t19 -15.5v0zM512 806l333 -166l-333 -166l-333 166l333 166v0zM922 252l-410 -205
+l-410 205q-16 6 -32.5 0.5t-22.5 -17.5q-6 -13 -1 -31t18 -25l427 -213q6 -7 10.5 -8t10.5 -1t10 0.5t7 4.5l427 213q16 6 21 22.5t-4 32.5q-7 16 -23 22t-28 0v0zM922 465l-410 -205l-410 205q-16 6 -32.5 1t-22.5 -18q-6 -16 -1 -32.5t18 -22.5l427 -214q6 -6 10.5 -7
+t10.5 -1t10 0.5t7 3.5l427 213q16 7 21 23.5t-4 32.5q-7 16 -23 21.5t-28 -0.5v0z" />
+    <glyph glyph-name="uniE97E" unicode="&#xe97e;" 
+d="M811 853h-598q-54 0 -91 -36.5t-37 -91.5v-597q0 -54 37 -91t91 -37h598q54 0 91 37t37 91v597q0 55 -37 91.5t-91 36.5zM213 768h598q19 0 30.5 -11.5t11.5 -31.5v-128h-682v128q0 20 11.5 31.5t30.5 11.5v0zM171 128v384h170v-427h-128q-19 0 -30.5 12t-11.5 31z
+M811 85h-384v427h426v-384q0 -19 -11.5 -31t-30.5 -12z" />
+    <glyph glyph-name="uniE97F" unicode="&#xe97f;" 
+d="M845 94q32 32 57 69q25 38 43 80t27 88t9 96q0 48 -9 94t-27 88t-43 80t-57 70v0v0v0v0q-32 32 -70 58q-38 25 -80.5 42.5t-88.5 27.5q-46 9 -94 9t-94 -9q-46 -10 -88.5 -27.5t-80.5 -42.5q-38 -26 -70 -58v0v0v0v0q-32 -32 -57 -70t-43 -80t-27 -88t-9 -94t9 -94
+t27 -88.5t43 -80.5t57 -70v0v0v0v0q32 -32 70 -57q37 -26 79 -43.5t88 -26.5q46 -10 96 -10q48 0 94 10q46 9 88.5 26.5t80.5 43.5q38 25 70 57v0v0v0v0zM811 188l-124 123q19 23 28.5 53.5t9.5 62.5t-9 63t-25 56l120 120q38 -48 61.5 -110t23.5 -129q0 -68 -22 -129.5
+t-63 -109.5v0zM384 427q0 54 37 91t91 37t91 -37t37 -91q0 -55 -37 -91.5t-91 -36.5t-91 36.5t-37 91.5zM751 725l-124 -123q-22 19 -52.5 28.5t-62.5 9.5t-63 -9t-56 -25l-120 119q48 42 110 64t129 22t129 -22t110 -64v0zM213 666l124 -124q-19 -23 -28.5 -53t-9.5 -62
+t9 -63t25 -57l-120 -119q-38 48 -61.5 109.5t-23.5 129.5q0 67 22 129t63 110v0zM273 128l124 124q22 -19 52.5 -29t62.5 -10t63 9t56 25l124 -123q-48 -39 -110 -62.5t-129 -23.5q-70 4 -132.5 26t-110.5 64v0z" />
+    <glyph glyph-name="uniE980" unicode="&#xe980;" 
+d="M900 819q-37 37 -83 55q-46 19 -94 19t-94 -19q-46 -18 -83 -55l-77 -72q-12 -13 -12 -30t12 -30q13 -13 30 -13t30 13l73 72q51 48 119 48t120 -48q51 -48 51.5 -119t-47.5 -119l-128 -128q-3 -4 -8.5 -9t-8.5 -9q-58 -41 -128 -32t-111 67q-10 12 -28.5 15t-31.5 -7
+q-13 -9 -15.5 -28t7.5 -32q38 -51 92.5 -76.5t111.5 -25.5q39 0 79 13t75 38q6 7 13 13t17 13l128 128q37 37 54 84q18 47 17 96.5t-21 95.5q-20 47 -59 82v0zM491 166l-73 -72q-51 -48 -119.5 -48t-119.5 48t-51.5 119.5t47.5 119.5l128 128l8.5 8.5l8.5 8.5q29 19 61 28
+t67 6q35 -6 63.5 -22.5t47.5 -45.5q10 -13 28.5 -15.5t31.5 6.5q12 10 15 28.5t-7 31.5q-32 42 -75 67t-91 31q-48 10 -97 -2.5t-91 -44.5l-12.5 -12.5t-13.5 -12.5l-128 -128q-36 -39 -54 -87q-18 -47 -17 -96.5t20 -95.5q19 -47 56 -84q38 -35 84.5 -54t94.5 -19t94.5 19
+t84.5 54l73 72q12 13 12 30t-12 30q-13 13 -32.5 15t-31.5 -11v0z" />
+    <glyph glyph-name="uniE981" unicode="&#xe981;" 
+d="M384 256h-128q-70 0 -120.5 50t-50.5 121q0 70 50.5 120t120.5 50h128q19 0 31 12t12 31t-12 31t-31 12h-128q-53 0 -99 -21q-47 -20 -82 -54.5t-55 -81.5t-20 -99q0 -53 20 -100q20 -46 55 -81t82 -55q46 -20 99 -20h128q19 0 31 11.5t12 30.5q0 20 -12 31.5t-31 11.5v0
+zM768 683h-128q-19 0 -31 -12t-12 -31t12 -31t31 -12h128q70 0 120.5 -50t50.5 -120q0 -71 -50.5 -121t-120.5 -50h-128q-19 0 -31 -11.5t-12 -31.5q0 -19 12 -30.5t31 -11.5h128q53 0 99 20q47 20 82 55t55 81q20 47 20 100q0 52 -20 99t-55 81.5t-82 54.5q-46 21 -99 21v0
+zM299 427q0 -20 11.5 -31.5t30.5 -11.5h342q19 0 30.5 11.5t11.5 31.5q0 19 -11.5 30.5t-30.5 11.5h-342q-19 0 -30.5 -11.5t-11.5 -30.5v0z" />
+    <glyph glyph-name="uniE982" unicode="&#xe982;" 
+d="M683 640q-63 0 -117 -23q-55 -24 -95 -64t-64 -95q-23 -54 -23 -117v-298q0 -20 11.5 -31.5t31.5 -11.5h170q20 0 31.5 11.5t11.5 31.5v298q0 20 11.5 31.5t31.5 11.5q19 0 30.5 -11.5t11.5 -31.5v-298q0 -20 12 -31.5t31 -11.5h171q19 0 30.5 11.5t11.5 31.5v298
+q0 63 -23 117q-23 55 -63.5 95t-94.5 64q-55 23 -117 23v0zM896 85h-85v256q0 55 -37 91.5t-91 36.5q-55 0 -91.5 -36.5t-36.5 -91.5v-256h-86v256q0 45 17 84q16 39 45 68t68 45q39 17 84 17q44 0 83 -17q39 -16 68 -45t45 -68q17 -39 17 -84v-256zM256 597h-171
+q-19 0 -30.5 -11.5t-11.5 -30.5v-512q0 -20 11.5 -31.5t30.5 -11.5h171q19 0 31 11.5t12 31.5v512q0 19 -12 30.5t-31 11.5zM213 85h-85v427h85v-427zM171 896q-55 0 -91.5 -37t-36.5 -91t36.5 -91t91.5 -37q54 0 91 37t37 91t-37 91t-91 37v0zM171 725q-20 0 -31.5 12
+t-11.5 31t11.5 31t31.5 12q19 0 30.5 -12t11.5 -31t-11.5 -31t-30.5 -12z" />
+    <glyph glyph-name="uniE983" unicode="&#xe983;" 
+d="M341 640h555q19 0 31 11.5t12 31.5q0 19 -12 30.5t-31 11.5h-555q-19 0 -30.5 -11.5t-11.5 -30.5q0 -20 11.5 -31.5t30.5 -11.5v0zM896 469h-555q-19 0 -30.5 -11.5t-11.5 -30.5q0 -20 11.5 -31.5t30.5 -11.5h555q19 0 31 11.5t12 31.5q0 19 -12 30.5t-31 11.5v0z
+M896 213h-555q-19 0 -30.5 -11.5t-11.5 -30.5q0 -20 11.5 -31.5t30.5 -11.5h555q19 0 31 11.5t12 31.5q0 19 -12 30.5t-31 11.5zM98 713q-6 -7 -9.5 -14t-3.5 -16q0 -10 3.5 -17t9.5 -13q7 -7 13.5 -10t16.5 -3t16.5 3t13.5 10q6 6 9.5 13t3.5 17q0 9 -3.5 16t-9.5 14
+q-13 12 -30 12t-30 -12v0zM98 457q-6 -7 -9.5 -14t-3.5 -16q0 -10 3.5 -17t9.5 -13q7 -7 13.5 -10t16.5 -3t16.5 3t13.5 10q6 6 9.5 13t3.5 17q0 9 -3.5 16t-9.5 14q-13 12 -30 12t-30 -12v0zM98 201q-6 -7 -9.5 -14t-3.5 -16q0 -10 3.5 -17t9.5 -13q7 -7 13.5 -10t16.5 -3
+t16.5 3t13.5 10q6 6 9.5 13t3.5 17q0 9 -3.5 16t-9.5 14q-13 12 -30 12t-30 -12v0z" />
+    <glyph glyph-name="uniE984" unicode="&#xe984;" 
+d="M512 896q-19 0 -31 -11.5t-12 -31.5v-170q0 -20 12 -31.5t31 -11.5t31 11.5t12 31.5v170q0 20 -12 31.5t-31 11.5zM512 213q-19 0 -31 -11.5t-12 -30.5v-171q0 -19 12 -31t31 -12t31 12t12 31v171q0 19 -12 30.5t-31 11.5zM239 759q-13 13 -30 13t-30 -13
+q-13 -12 -13 -29t13 -30l120 -120q6 -6 14.5 -9.5t15.5 -3.5q6 0 14.5 3.5t14.5 9.5q13 13 13 30t-13 30l-119 119v0zM721 277q-13 13 -30 13t-30 -13q-12 -12 -12 -29.5t12 -29.5l120 -120q6 -6 14.5 -9.5t15.5 -3.5q6 0 14.5 3.5t15.5 9.5q12 13 12 30t-12 30l-120 119v0z
+M299 427q0 19 -12 30.5t-31 11.5h-171q-19 0 -30.5 -11.5t-11.5 -30.5q0 -20 11.5 -31.5t30.5 -11.5h171q19 0 31 11.5t12 31.5zM939 469h-171q-19 0 -31 -11.5t-12 -30.5q0 -20 12 -31.5t31 -11.5h171q19 0 30.5 11.5t11.5 31.5q0 19 -11.5 30.5t-30.5 11.5v0zM303 277
+l-120 -119q-12 -13 -12 -30t12 -30q7 -6 15.5 -9.5t14.5 -3.5q7 0 15.5 3.5t14.5 9.5l120 120q12 12 12 29.5t-12 29.5q-13 13 -31.5 13t-28.5 -13v0zM691 563q10 0 17 3.5t13 9.5l120 119q12 13 12 30t-12 30q-13 13 -30 13t-30 -13l-120 -119q-12 -13 -12 -30t12 -30
+q7 -6 15.5 -9.5t14.5 -3.5v0z" />
+    <glyph glyph-name="uniE985" unicode="&#xe985;" 
+d="M811 512h-43v128q0 53 -20 99q-20 47 -55 82t-82 55q-46 20 -99 20t-99 -20q-47 -20 -82 -55t-55 -82q-20 -46 -20 -99v-128h-43q-54 0 -91 -37t-37 -91v-299q0 -54 37 -91t91 -37h598q54 0 91 37t37 91v299q0 54 -37 91t-91 37v0zM341 640q0 70 50.5 120.5t120.5 50.5
+t120.5 -50.5t50.5 -120.5v-128h-342v128v0zM853 85q0 -19 -11.5 -30.5t-30.5 -11.5h-598q-19 0 -30.5 11.5t-11.5 30.5v299q0 19 11.5 31t30.5 12h598q19 0 30.5 -12t11.5 -31v-299v0z" />
+    <glyph glyph-name="uniE986" unicode="&#xe986;" 
+d="M811 853h-171q-19 0 -31 -11.5t-12 -30.5q0 -20 12 -31.5t31 -11.5h171q19 0 30.5 -11.5t11.5 -31.5v-597q0 -19 -11.5 -31t-30.5 -12h-171q-19 0 -31 -11.5t-12 -30.5q0 -20 12 -31.5t31 -11.5h171q54 0 91 37t37 91v597q0 55 -37 91.5t-91 36.5v0zM678 410q4 6 4 15
+t-4 19q-3 3 -4 6t-4 7l-213 213q-13 13 -30 13t-30 -13t-13 -30t13 -30l141 -141h-410q-19 0 -31 -11.5t-12 -30.5q0 -20 12 -31.5t31 -11.5h410l-141 -141q-13 -13 -13 -30t13 -30q6 -6 13 -9t17 -3q9 0 16 3t14 9l213 214q3 3 5.5 6t2.5 7v0z" />
+    <glyph glyph-name="uniE987" unicode="&#xe987;" 
+d="M384 85h-171q-19 0 -30.5 12t-11.5 31v597q0 20 11.5 31.5t30.5 11.5h171q19 0 31 11.5t12 31.5q0 19 -12 30.5t-31 11.5h-171q-54 0 -91 -36.5t-37 -91.5v-597q0 -54 37 -91t91 -37h171q19 0 31 11.5t12 31.5q0 19 -12 30.5t-31 11.5v0zM934 410q4 6 4 15t-4 19
+q-3 3 -4 6t-4 7l-213 213q-13 13 -30 13t-30 -13t-13 -30t13 -30l141 -141h-410q-19 0 -31 -11.5t-12 -30.5q0 -20 12 -31.5t31 -11.5h410l-141 -141q-13 -13 -13 -30t13 -30q6 -6 14.5 -9t15.5 -3q6 0 14.5 3t15.5 9l213 214q3 3 5.5 6t2.5 7v0z" />
+    <glyph glyph-name="uniE988" unicode="&#xe988;" 
+d="M853 811h-682q-55 0 -91.5 -37t-36.5 -91v-512q0 -55 36.5 -91.5t91.5 -36.5h682q55 0 91.5 36.5t36.5 91.5v512q0 54 -36.5 91t-91.5 37zM171 725h682q13 0 22.5 -6t16.5 -19l-380 -265l-380 265q7 13 16.5 19t22.5 6v0zM853 128h-682q-20 0 -31.5 11.5t-11.5 31.5v431
+l358 -252q7 -3 13.5 -6t12.5 -3t12.5 3t13.5 6l358 252v-431q0 -20 -11.5 -31.5t-31.5 -11.5v0z" />
+    <glyph glyph-name="uniE989" unicode="&#xe989;" 
+d="M1003 892q-10 6 -21.5 6t-21.5 -6l-277 -162l-325 162v0v0h-0.5h-3.5q-3 3 -6 3.5t-7 0.5v0v0v0v0q-3 0 -6.5 -0.5t-10.5 -3.5h-0.5h-3.5h-2h-2l-299 -171q-6 -6 -11.5 -16t-5.5 -22v-683q0 -13 6 -22.5t15 -15.5q10 -7 21.5 -7t21.5 7l277 162l320 -162v0v0
+q7 -4 11 -4.5t11 -0.5q3 0 6 0.5t6 4.5h2.5h2.5h0.5h3.5l299 170q9 7 15 16.5t6 22.5v682q0 13 -6 22.5t-15 16.5v0zM384 785l256 -128v-589l-256 128v589zM85 657l214 124v-585l-214 -123v584zM939 196l-214 -123v584l214 124v-585v0z" />
+    <glyph glyph-name="uniE98A" unicode="&#xe98a;" 
+d="M512 939q-88 0 -166 -34q-77 -34 -135 -92t-92 -135q-34 -78 -34 -166q0 -118 61 -224t135 -186t137 -128l68 -51q7 -3 13.5 -5.5t12.5 -2.5t12.5 2.5t13.5 5.5l68 51q63 48 137 128t135 186t61 224q0 88 -34 166q-34 77 -92 135t-135 92q-78 34 -166 34v0zM512 9
+q-30 22 -88 71q-57 49 -113 116t-98 149q-42 81 -42 167q0 70 27 133q27 62 73 108t108 73q63 27 133 27t133 -27q62 -27 108 -73t73 -108q27 -63 27 -133q0 -86 -42 -167q-42 -82 -98 -149t-113 -116q-58 -49 -88 -71zM512 683q-70 0 -120.5 -50.5t-50.5 -120.5
+t50.5 -120.5t120.5 -50.5t120.5 50.5t50.5 120.5t-50.5 120.5t-120.5 50.5zM512 427q-35 0 -60 25t-25 60t25 60t60 25t60 -25t25 -60t-25 -60t-60 -25z" />
+    <glyph glyph-name="uniE98B" unicode="&#xe98b;" 
+d="M341 853h-128q-54 0 -91 -36.5t-37 -91.5v-128q0 -19 12 -30.5t31 -11.5t31 11.5t12 30.5v128q0 20 11.5 31.5t30.5 11.5h128q20 0 31.5 11.5t11.5 31.5q0 19 -11.5 30.5t-31.5 11.5v0zM341 85h-128q-19 0 -30.5 12t-11.5 31v128q0 19 -12 31t-31 12t-31 -12t-12 -31
+v-128q0 -54 37 -91t91 -37h128q20 0 31.5 11.5t11.5 31.5q0 19 -11.5 30.5t-31.5 11.5v0zM896 299q-19 0 -31 -12t-12 -31v-128q0 -19 -11.5 -31t-30.5 -12h-128q-20 0 -31.5 -11.5t-11.5 -30.5q0 -20 11.5 -31.5t31.5 -11.5h128q54 0 91 37t37 91v128q0 19 -12 31t-31 12v0
+zM811 853h-128q-20 0 -31.5 -11.5t-11.5 -30.5q0 -20 11.5 -31.5t31.5 -11.5h128q19 0 30.5 -11.5t11.5 -31.5v-128q0 -19 12 -30.5t31 -11.5t31 11.5t12 30.5v128q0 55 -37 91.5t-91 36.5v0z" />
+    <glyph glyph-name="uniE98C" unicode="&#xe98c;" 
+d="M934 828q-3 6 -9 12t-12 9q-3 3 -8.5 3.5t-8.5 0.5h-256q-19 0 -31 -11.5t-12 -30.5q0 -20 12 -31.5t31 -11.5h154l-227 -226q-12 -13 -12 -30t12 -30q7 -6 15.5 -9.5t14.5 -3.5q7 0 15.5 3.5t14.5 9.5l226 226v-153q0 -20 12 -31.5t31 -11.5t31 11.5t12 31.5v256
+q0 3 -0.5 8.5t-4.5 8.5v0zM397 371l-226 -226v154q0 19 -12 30.5t-31 11.5t-31 -11.5t-12 -30.5v-256q0 -4 0.5 -9t4.5 -8q3 -7 9 -13t12 -9q3 -3 8.5 -3.5t8.5 -0.5h256q19 0 31 11.5t12 31.5q0 19 -12 30.5t-31 11.5h-154l227 226q12 13 12 30t-12 30q-13 13 -30 13
+t-30 -13z" />
+    <glyph glyph-name="uniE98D" unicode="&#xe98d;" 
+d="M896 469h-768q-19 0 -31 -11.5t-12 -30.5q0 -20 12 -31.5t31 -11.5h768q19 0 31 11.5t12 31.5q0 19 -12 30.5t-31 11.5v0zM128 640h768q19 0 31 11.5t12 31.5q0 19 -12 30.5t-31 11.5h-768q-19 0 -31 -11.5t-12 -30.5q0 -20 12 -31.5t31 -11.5zM896 213h-768
+q-19 0 -31 -11.5t-12 -30.5q0 -20 12 -31.5t31 -11.5h768q19 0 31 11.5t12 31.5q0 19 -12 30.5t-31 11.5z" />
+    <glyph glyph-name="uniE98E" unicode="&#xe98e;" 
+d="M939 474q-5 76 -36 144q-32 68 -83 119t-119 82t-146 34h-22v0v0q-48 0 -92.5 -10t-86.5 -32q-53 -26 -95 -64t-71 -85.5t-44 -101.5q-16 -54 -16 -112q0 -42 9 -85t25 -81l-77 -227q-3 -12 -0.5 -24t9.5 -18q9 -7 16.5 -10t17.5 -3q3 0 6.5 0.5t6.5 3.5l226 77
+q38 -16 80 -25t86 -9q58 0 112 15q54 16 101.5 44.5t85.5 70.5q38 41 64 92q22 41 32.5 88t10.5 95v22v0zM853 448v0v0q0 -38 -9 -74t-25 -71q-21 -42 -50 -74q-30 -33 -67 -55.5t-80 -33.5q-42 -12 -89 -12q-38 0 -73 9t-67 25q-7 3 -16 3.5t-19 0.5l-162 -55l56 162
+q3 10 2.5 17.5t-7.5 16.5q-16 32 -25 69t-9 72q0 46 13 89q12 43 35.5 79.5t56.5 66.5t75 51q32 16 68.5 25t71.5 9v0v0h17q61 -3 114 -27q53 -25 93.5 -65t65.5 -93q26 -53 30 -114v-21v0z" />
+    <glyph glyph-name="uniE98F" unicode="&#xe98f;" 
+d="M811 853h-598q-54 0 -91 -36.5t-37 -91.5v-682q0 -13 6.5 -24.5t19.5 -14.5q3 -3 8.5 -3.5t8.5 -0.5q10 0 16.5 3t13.5 10l158 158h495q54 0 91 36.5t37 91.5v426q0 55 -37 91.5t-91 36.5v0zM853 299q0 -20 -11.5 -31.5t-30.5 -11.5h-512q-10 0 -17 -3t-13 -10l-98 -98
+v580q0 20 11.5 31.5t30.5 11.5h598q19 0 30.5 -11.5t11.5 -31.5v-426z" />
+    <glyph glyph-name="uniE990" unicode="&#xe990;" 
+d="M512 256q70 0 120.5 50t50.5 121v341q0 70 -50.5 120.5t-120.5 50.5t-120.5 -50.5t-50.5 -120.5v-341q0 -71 50.5 -121t120.5 -50zM427 768q0 35 25 60t60 25t60 -25t25 -60v-341q0 -36 -25 -61t-60 -25t-60 25t-25 61v341v0zM811 555q-20 0 -31.5 -12t-11.5 -31v-85
+q0 -53 -20 -100q-20 -46 -55 -81t-82 -55q-46 -20 -99 -20t-99 20q-47 20 -82 55t-55 81q-20 47 -20 100v85q0 19 -11.5 31t-31.5 12q-19 0 -30.5 -12t-11.5 -31v-85q0 -66 23 -124t63.5 -102.5t94.5 -73.5q55 -29 117 -37v-90h-128q-19 0 -30.5 -11.5t-11.5 -31.5
+q0 -19 11.5 -30.5t30.5 -11.5h342q19 0 30.5 11.5t11.5 30.5q0 20 -11.5 31.5t-30.5 11.5h-128v90q62 8 117 37q54 30 94.5 75t63.5 103t23 122v85q0 19 -11.5 31t-30.5 12v0z" />
+    <glyph glyph-name="uniE991" unicode="&#xe991;" 
+d="M1011 -13l-252 252q0 3 -2 4t-2 4l-2 2.5l-2 2.5l-337 333v0v0l-341 341q-13 13 -30 13t-30 -13t-13 -30t13 -30l328 -328v-111q0 -36 13 -66.5t39 -53.5q22 -25 53 -38t66 -13v0v0q22 0 44 6.5t41 19.5l64 -64q-32 -23 -67 -35t-73 -12h-5h-4h-4h-5q-48 0 -91.5 18.5
+t-78.5 53.5q-39 35 -58 83.5t-19 100.5v85q0 19 -11.5 31t-31.5 12q-19 0 -30.5 -12t-11.5 -31v-85q0 -71 27 -135.5t75 -112.5q42 -41 92 -64t104 -30v-85h-128q-19 0 -30.5 -11.5t-11.5 -31.5q0 -19 11.5 -30.5t30.5 -11.5h342q19 0 30.5 11.5t11.5 30.5q0 20 -11.5 31.5
+t-30.5 11.5h-128v85q44 7 87.5 23.5t78.5 45.5l230 -231q7 -6 15.5 -9.5t14.5 -3.5q7 0 15.5 3.5t14.5 9.5q13 16 13 33.5t-13 30.5v0zM512 341v0v0q-16 0 -31.5 6.5t-28.5 19.5t-19 28.5t-6 31.5v25l106 -106q-6 -4 -10.5 -4.5t-10.5 -0.5v0zM380 751q16 -3 29.5 7.5
+t17.5 26.5q6 29 29.5 48.5t55.5 19.5v0v0q16 0 31.5 -6t28.5 -19t19 -28.5t6 -31.5v-226q0 -19 12 -31t31 -12t31 12t12 31v226q0 35 -13 66t-39 53q-22 26 -53 39t-66 13v0v0q-61 0 -107.5 -38t-58.5 -99q-4 -16 5.5 -32t28.5 -19v0zM798 333h4h4q16 0 28 9t15 25t3.5 30
+t0.5 30v85q0 19 -11.5 31t-30.5 12q-20 0 -31.5 -12t-11.5 -31v-85q0 -13 -0.5 -23.5t-3.5 -23.5q-3 -16 7.5 -30t26.5 -17v0z" />
+    <glyph glyph-name="uniE992" unicode="&#xe992;" 
+d="M256 299h-128q-19 0 -31 -12t-12 -31t12 -31t31 -12h128q19 0 31 -11.5t12 -30.5v-128q0 -20 11.5 -31.5t30.5 -11.5q20 0 31.5 11.5t11.5 31.5v128q0 54 -37 91t-91 37v0zM768 555h128q19 0 31 11.5t12 30.5q0 20 -12 31.5t-31 11.5h-128q-19 0 -31 11.5t-12 31.5v128
+q0 19 -11.5 30.5t-30.5 11.5q-20 0 -31.5 -11.5t-11.5 -30.5v-128q0 -55 37 -91.5t91 -36.5v0zM896 299h-128q-54 0 -91 -37t-37 -91v-128q0 -20 11.5 -31.5t31.5 -11.5q19 0 30.5 11.5t11.5 31.5v128q0 19 12 30.5t31 11.5h128q19 0 31 12t12 31t-12 31t-31 12v0zM341 853
+q-19 0 -30.5 -11.5t-11.5 -30.5v-128q0 -20 -12 -31.5t-31 -11.5h-128q-19 0 -31 -11.5t-12 -31.5q0 -19 12 -30.5t31 -11.5h128q54 0 91 36.5t37 91.5v128q0 19 -11.5 30.5t-31.5 11.5v0z" />
+    <glyph glyph-name="uniE993" unicode="&#xe993;" 
+d="M444 380q-3 3 -8.5 3.5t-8.5 0.5h-256q-20 0 -31.5 -11.5t-11.5 -31.5q0 -19 11.5 -30.5t31.5 -11.5h153l-226 -226q-13 -13 -13 -30t13 -30q7 -7 13.5 -10t16.5 -3t16.5 3t13.5 10l226 226v-154q0 -19 11.5 -30.5t31.5 -11.5q19 0 30.5 11.5t11.5 30.5v256q0 4 -0.5 9
+t-3.5 8q-3 7 -9 13t-12 9v0zM926 841q-13 12 -30 12t-30 -12l-226 -227v154q0 19 -11.5 31t-31.5 12q-19 0 -30.5 -12t-11.5 -31v-256q0 -3 0.5 -8.5t3.5 -8.5q3 -6 9 -12t12 -9q3 -4 8.5 -4.5t8.5 -0.5h256q20 0 31.5 12t11.5 31t-11.5 31t-31.5 12h-153l226 226
+q13 13 13 30t-13 30z" />
+    <glyph glyph-name="uniE994" unicode="&#xe994;" 
+d="M811 469h-598q-19 0 -30.5 -11.5t-11.5 -30.5q0 -20 11.5 -31.5t30.5 -11.5h598q19 0 30.5 11.5t11.5 31.5q0 19 -11.5 30.5t-30.5 11.5v0z" />
+    <glyph glyph-name="uniE995" unicode="&#xe995;" 
+d="M512 896q-98 0 -183 -37q-86 -36 -149.5 -100t-100.5 -149q-36 -86 -36 -183q0 -98 36 -183q37 -86 100.5 -149.5t149.5 -100.5q85 -37 183 -37t183 37q86 37 149.5 100.5t100.5 149.5q36 85 36 183q0 97 -36 183q-37 85 -100.5 149t-149.5 100q-85 37 -183 37zM512 43
+q-80 0 -150 30t-122 82t-82 122t-30 150t30 150t82 122t122 82t150 30t150 -30t122 -82t82 -122t30 -150t-30 -150t-82 -122t-122 -82t-150 -30zM683 469h-342q-19 0 -30.5 -11.5t-11.5 -30.5q0 -20 11.5 -31.5t30.5 -11.5h342q19 0 30.5 11.5t11.5 31.5q0 19 -11.5 30.5
+t-30.5 11.5v0z" />
+    <glyph glyph-name="uniE996" unicode="&#xe996;" 
+d="M811 853h-598q-54 0 -91 -36.5t-37 -91.5v-597q0 -54 37 -91t91 -37h598q54 0 91 37t37 91v597q0 55 -37 91.5t-91 36.5zM853 128q0 -19 -11.5 -31t-30.5 -12h-598q-19 0 -30.5 12t-11.5 31v597q0 20 11.5 31.5t30.5 11.5h598q19 0 30.5 -11.5t11.5 -31.5v-597zM683 469
+h-342q-19 0 -30.5 -11.5t-11.5 -30.5q0 -20 11.5 -31.5t30.5 -11.5h342q19 0 30.5 11.5t11.5 31.5q0 19 -11.5 30.5t-30.5 11.5v0z" />
+    <glyph glyph-name="uniE997" unicode="&#xe997;" 
+d="M853 853h-682q-55 0 -91.5 -36.5t-36.5 -91.5v-426q0 -55 36.5 -91.5t91.5 -36.5h298v-86h-128q-19 0 -30.5 -11.5t-11.5 -30.5q0 -20 11.5 -31.5t30.5 -11.5h342q19 0 30.5 11.5t11.5 31.5q0 19 -11.5 30.5t-30.5 11.5h-128v86h298q55 0 91.5 36.5t36.5 91.5v426
+q0 55 -36.5 91.5t-91.5 36.5v0zM896 299q0 -20 -11.5 -31.5t-31.5 -11.5h-682q-20 0 -31.5 11.5t-11.5 31.5v426q0 20 11.5 31.5t31.5 11.5h682q20 0 31.5 -11.5t11.5 -31.5v-426z" />
+    <glyph glyph-name="uniE998" unicode="&#xe998;" 
+d="M917 431q-9 6 -23 5.5t-24 -9.5q-33 -26 -72 -39t-79 -13t-79 13t-73 39q-43 32 -69 75q-26 44 -33.5 92.5t3.5 97.5q12 50 44 93q6 10 8.5 22t-4.5 25q-6 10 -18 15.5t-24 5.5q-77 -6 -144 -38q-68 -32 -119 -83t-83 -119q-32 -67 -38 -144q-8 -88 18 -168
+q26 -81 78 -144t126 -104t162 -49h18.5h19.5q74 0 144.5 25t128.5 73q34 28 60 60q26 33 45.5 69.5t31.5 77.5q12 40 17 84q0 9 -6 20.5t-16 17.5v0zM734 166q-51 -44 -115.5 -63.5t-132.5 -12.5q-70 6 -129 38q-59 33 -101 83.5t-62 114.5q-21 64 -15 135q5 49 23 95
+q19 45 48 83t68 67q38 29 83 45q-19 -51 -20 -106q-1 -54 14.5 -106t46.5 -99q32 -46 79 -82q35 -25 74 -41q40 -16 81 -22t83 -2t82 18q-20 -41 -46 -78.5t-61 -66.5z" />
+    <glyph glyph-name="uniE999" unicode="&#xe999;" 
+d="M597 427q0 -36 -25 -61t-60 -25t-60 25t-25 61q0 35 25 60t60 25t60 -25t25 -60zM896 427q0 -36 -25 -61t-60 -25q-36 0 -61 25t-25 61q0 35 25 60t61 25q35 0 60 -25t25 -60zM299 427q0 -36 -25 -61t-61 -25q-35 0 -60 25t-25 61q0 35 25 60t60 25q36 0 61 -25t25 -60z
+" />
+    <glyph glyph-name="uniE99A" unicode="&#xe99a;" 
+d="M597 427q0 -36 -25 -61t-60 -25t-60 25t-25 61q0 35 25 60t60 25t60 -25t25 -60zM597 725q0 -35 -25 -60t-60 -25t-60 25t-25 60q0 36 25 61t60 25t60 -25t25 -61zM597 128q0 -35 -25 -60t-60 -25t-60 25t-25 60t25 60t60 25t60 -25t25 -60z" />
+    <glyph glyph-name="uniE99B" unicode="&#xe99b;" 
+d="M977 410q3 6 3 15t-3 19q-3 3 -4 6t-4 7l-128 128q-13 12 -30 12t-30 -12q-13 -13 -13 -30t13 -30l55 -56h-281v282l55 -56q7 -6 13.5 -9t16.5 -3t16.5 3t13.5 9q13 13 13 30t-13 30l-128 128q-3 3 -6.5 6t-6.5 3q-6 3 -15 3t-19 -3q-3 -3 -6.5 -4.5t-6.5 -4.5l-128 -128
+q-13 -13 -13 -30t13 -30q13 -12 30 -12t30 12l55 56v-282h-281l55 56q13 13 13 30t-13 30q-13 12 -30 12t-30 -12l-128 -128q-3 -4 -5.5 -7t-2.5 -6q-3 -7 -3 -16t3 -18q3 -4 4 -7t4 -6l128 -128q7 -7 14 -10t16 -3q10 0 17 3t13 10q13 13 13 30t-13 30l-55 55h281v-282
+l-55 56q-13 13 -30 13t-30 -13t-13 -30t13 -30l128 -128q3 -3 6.5 -5.5t6.5 -2.5q3 -4 8.5 -4.5t8.5 -0.5t8.5 0.5t8.5 4.5q3 3 6.5 4t6.5 4l128 128q13 13 13 30t-13 30t-30 13t-30 -13l-55 -56v282h281l-55 -55q-13 -13 -13 -30t13 -30q6 -7 14.5 -10t15.5 -3q6 0 14.5 3
+t15.5 10l128 128q3 3 5.5 6t2.5 7v0z" />
+    <glyph glyph-name="uniE99C" unicode="&#xe99c;" 
+d="M922 845q-7 6 -16 7t-19 1l-512 -85q-12 -3 -23 -15t-11 -28v-469h-128q-54 0 -91 -37t-37 -91t37 -91t91 -37h86q54 0 91 37t37 91v563l426 73v-423h-128q-54 0 -91 -36.5t-37 -91.5q0 -54 37 -91t91 -37h86q54 0 91 37t37 91v598q0 9 -4 18t-13 16v0zM341 128
+q0 -19 -11.5 -31t-30.5 -12h-86q-19 0 -30.5 12t-11.5 31t11.5 31t30.5 12h128v-43v0zM853 213q0 -19 -11.5 -30.5t-30.5 -11.5h-86q-19 0 -30.5 11.5t-11.5 30.5q0 20 11.5 31.5t30.5 11.5h128v-43z" />
+    <glyph glyph-name="uniE99D" unicode="&#xe99d;" 
+d="M969 883q-10 10 -22.5 12.5t-24.5 -3.5l-811 -384q-13 -7 -19.5 -18.5t-6.5 -24.5t9 -24t25 -14l316 -81l81 -316q3 -13 14.5 -22t24.5 -12h2h2q13 0 22.5 6.5t15.5 18.5l384 811q4 16 0.5 29t-12.5 22v0zM567 171l-55 222q-3 12 -11.5 19t-18.5 10l-226 60l593 282z" />
+    <glyph glyph-name="uniE99E" unicode="&#xe99e;" 
+d="M849 55l-299 811q-3 13 -14 21.5t-24 8.5t-24 -8.5t-14 -21.5l-299 -811q-3 -12 0 -24.5t13 -21.5q9 -10 23 -10.5t24 5.5l277 158l277 -158q7 -3 11 -3.5t11 -0.5q6 0 14 2.5t11 6.5q13 9 16 23t-3 23v0zM533 252q-6 3 -10.5 3.5t-10.5 0.5t-10.5 -0.5t-10.5 -3.5
+l-197 -111l218 589l218 -593l-197 115v0z" />
+    <glyph glyph-name="uniE99F" unicode="&#xe99f;" 
+d="M969 631l-252 252q-3 7 -11.5 10t-18.5 3h-350q-10 0 -18.5 -3t-11.5 -10l-252 -252q-6 -3 -9 -11.5t-3 -17.5v-355q0 -9 3 -16t9 -13l252 -252q3 -3 11.5 -6t18.5 -3h354q10 0 17 3.5t13 9.5l252 252q6 6 9.5 13t3.5 17v350q-4 9 -7.5 17.5t-9.5 11.5v0zM896 269
+l-226 -226h-316l-226 226v316l226 226h320l222 -226v-316v0z" />
+    <glyph glyph-name="uniE9A0" unicode="&#xe9a0;" 
+d="M909 742l-342 171v0v0q-25 13 -55.5 13t-59.5 -13l-341 -171q-32 -16 -50 -45.5t-18 -65.5v-405q0 -35 18.5 -67t53.5 -48l342 -171q12 -6 27 -9.5t28 -3.5q16 0 29.5 3.5t25.5 9.5l342 171q32 16 52 46.5t20 68.5v405q0 36 -18.5 65.5t-53.5 45.5zM495 841q3 3 8.5 3.5
+t8.5 0.5q6 0 10 -0.5t7 -3.5l316 -158l-120 -60l-332 166zM512 516l-333 167l120 59l332 -166zM149 183q-9 7 -15 18t-6 21v392l341 -170v-418l-320 157v0zM870 183l-315 -157v418l341 170v-392q0 -13 -6.5 -22.5t-19.5 -16.5z" />
+    <glyph glyph-name="uniE9A1" unicode="&#xe9a1;" 
+d="M943 499q-13 13 -30 13t-30 -13l-392 -392q-32 -32 -72 -48q-39 -16 -80 -16t-80 16t-71 48t-48 71t-16 80t16 80q16 40 48 72l392 392q35 35 89.5 35t89.5 -35q39 -38 39 -91t-39 -88l-392 -393q-13 -12 -30 -12t-30 12q-3 4 -5.5 10t-2.5 16t3 16.5t9 13.5l363 363
+q13 12 13 29t-13 30t-30 13t-30 -13l-362 -362q-19 -16 -29 -40t-10 -50t10 -50t29 -40q38 -38 91 -38t88 38l392 393q32 32 48 71q16 40 16 80.5t-16 80.5q-16 39 -48 71q-28 29 -67.5 46.5t-81.5 17.5q-41 0 -80.5 -16t-68.5 -48l-393 -393q-43 -43 -65 -98
+q-21 -55 -21 -112.5t21 -113.5q22 -55 65 -98q45 -45 100.5 -65t113.5 -20q57 0 112.5 21.5t100.5 63.5l392 392q7 10 6 28.5t-14 31.5v0z" />
+    <glyph glyph-name="uniE9A2" unicode="&#xe9a2;" 
+d="M427 811h-171q-19 0 -31 -12t-12 -31v-683q0 -19 12 -30.5t31 -11.5h171q19 0 30.5 11.5t11.5 30.5v683q0 19 -11.5 31t-30.5 12v0zM384 128h-85v597h85v-597zM768 811h-171q-19 0 -30.5 -12t-11.5 -31v-683q0 -19 11.5 -30.5t30.5 -11.5h171q19 0 31 11.5t12 30.5v683
+q0 19 -12 31t-31 12v0zM725 128h-85v597h85v-597z" />
+    <glyph glyph-name="uniE9A3" unicode="&#xe9a3;" 
+d="M512 896q-98 0 -183 -37q-86 -36 -149.5 -100t-100.5 -149q-36 -86 -36 -183q0 -98 36 -183q37 -86 100.5 -149.5t149.5 -100.5q85 -37 183 -37t183 37q86 37 149.5 100.5t100.5 149.5q36 85 36 183q0 97 -36 183q-37 85 -100.5 149t-149.5 100q-85 37 -183 37zM512 43
+q-80 0 -150 30t-122 82t-82 122t-30 150t30 150t82 122t122 82t150 30t150 -30t122 -82t82 -122t30 -150t-30 -150t-82 -122t-122 -82t-150 -30zM427 597q-20 0 -31.5 -11.5t-11.5 -30.5v-256q0 -20 11.5 -31.5t31.5 -11.5q19 0 30.5 11.5t11.5 31.5v256q0 19 -11.5 30.5
+t-30.5 11.5v0zM597 597q-19 0 -30.5 -11.5t-11.5 -30.5v-256q0 -20 11.5 -31.5t30.5 -11.5q20 0 31.5 11.5t11.5 31.5v256q0 19 -11.5 30.5t-31.5 11.5z" />
+    <glyph glyph-name="uniE9A4" unicode="&#xe9a4;" 
+d="M841 755q-13 13 -30 13t-30 -13l-598 -597q-12 -13 -12 -30t12 -30q7 -6 14 -9.5t16 -3.5q10 0 17 3.5t13 9.5l598 597q12 13 12 30t-12 30v0zM277 512q61 0 105.5 44.5t44.5 104.5q0 61 -44.5 105.5t-105.5 44.5q-60 0 -104.5 -44.5t-44.5 -105.5q0 -60 44.5 -104.5
+t104.5 -44.5v0zM277 725q26 0 45 -19t19 -45q0 -25 -19 -44.5t-45 -19.5q-25 0 -44.5 19.5t-19.5 44.5q0 26 19.5 45t44.5 19zM747 341q-61 0 -105.5 -44t-44.5 -105t44.5 -105t105.5 -44q60 0 104.5 44t44.5 105t-44.5 105t-104.5 44zM747 128q-26 0 -45 19t-19 45t19 45
+t45 19q25 0 44.5 -19t19.5 -45t-19.5 -45t-44.5 -19z" />
+    <glyph glyph-name="uniE9A5" unicode="&#xe9a5;" 
+d="M870 346q-28 3 -56.5 9.5t-54.5 15.5q-35 13 -71 4.5t-65 -34.5l-30 -30q-58 39 -108 87.5t-88 109.5l30 30q25 25 33.5 63t-3.5 73q-10 26 -16.5 54t-9.5 57q-6 48 -43 79.5t-85 31.5v0v0h-128h-6.5h-6.5q-25 -3 -47 -15.5t-38 -31.5t-23 -43.5t-7 -50.5q5 -51 16 -102
+q12 -51 29.5 -100t40.5 -96t50 -90q26 -40 56 -77q30 -38 64 -72t72 -64q37 -30 77 -56q43 -29 90 -52t96.5 -40t99.5 -28q51 -12 103 -16h6h6v0v0q26 0 50 9.5t40 28.5t27 41.5t11 48.5v128q0 48 -31.5 84.5t-79.5 43.5v0zM896 218v-128q0 -10 -3 -17t-10 -13
+q-6 -7 -13.5 -10t-20.5 -3q-46 5 -92 15q-45 10 -89 26t-87 36q-42 21 -82 47q-37 22 -70 49q-34 27 -64.5 58t-57.5 65t-51 71q-26 40 -46 82q-21 42 -36.5 86t-26.5 90q-10 45 -15 92q0 9 3 16l6 14q6 6 15 11.5t19 5.5h128v0v0q16 0 27.5 -11.5t15.5 -27.5
+q3 -32 12 -65.5t22 -62.5q3 -13 0.5 -25t-9.5 -22l-55 -51q-10 -9 -12.5 -23.5t3.5 -27.5q24 -43 54 -82q29 -39 63.5 -73.5t73.5 -64.5q39 -29 82 -53q13 -7 27.5 -5.5t23.5 13.5l56 56q10 9 22 12t25 -4q32 -12 64 -19.5t64 -14.5q13 -3 23.5 -14.5t10.5 -27.5v0v0v0z" />
+    <glyph glyph-name="uniE9A6" unicode="&#xe9a6;" 
+d="M870 346q-28 3 -56.5 9.5t-54.5 15.5q-35 13 -71 4.5t-65 -34.5l-30 -30q-58 39 -108 87.5t-88 109.5l30 30q25 25 33.5 63t-3.5 73q-10 26 -16.5 54t-9.5 57q-6 48 -43 79.5t-85 31.5v0v0h-128h-6.5h-6.5q-25 -3 -47 -15.5t-38 -31.5t-23 -43.5t-7 -50.5q5 -51 16 -102
+q12 -51 29.5 -100t40.5 -96t50 -90q26 -40 56 -77q30 -38 64 -72t72 -64q37 -30 77 -56q43 -29 90 -52t96.5 -40t99.5 -28q51 -12 103 -16h6h6v0v0q26 0 50 9.5t40 28.5t27 41.5t11 48.5v128q0 48 -31.5 84.5t-79.5 43.5v0zM896 218v-128q0 -10 -3 -17t-10 -13
+q-6 -7 -13.5 -10t-20.5 -3q-46 5 -92 15q-45 10 -89 26t-87 36q-42 21 -82 47q-37 22 -70 49q-34 27 -64.5 58t-57.5 65t-51 71q-26 40 -46 82q-21 42 -36.5 86t-26.5 90q-10 45 -15 92q0 9 3 16l6 14q6 6 15 11.5t19 5.5h128v0v0q16 0 27.5 -11.5t15.5 -27.5
+q3 -32 12 -65.5t22 -62.5q3 -13 0.5 -25t-9.5 -22l-55 -51q-10 -9 -12.5 -23.5t3.5 -27.5q24 -43 54 -82q29 -39 63.5 -73.5t73.5 -64.5q39 -29 82 -53q13 -7 27.5 -5.5t23.5 13.5l56 56q10 9 22 12t25 -4q32 -12 64 -19.5t64 -14.5q13 -3 23.5 -14.5t10.5 -27.5v0v0v0z
+M649 939q-16 3 -30 -8.5t-17 -30.5q-4 -16 7.5 -29.5t30.5 -17.5q59 -6 111 -31q52 -26 92 -66t66 -92q26 -53 34 -114q3 -16 15 -27t28 -11h2h2q16 3 27 17t11 30q-9 75 -42 141t-83.5 116t-115.5 83q-65 32 -138 40v0zM768 550q3 -16 15 -25t28 -9h4h4q16 3 27 17.5
+t7 33.5q-16 77 -69.5 132.5t-130.5 68.5q-16 3 -32 -6t-19 -28q-4 -19 5.5 -33.5t28.5 -17.5q51 -10 86.5 -45.5t45.5 -87.5z" />
+    <glyph glyph-name="uniE9A7" unicode="&#xe9a7;" 
+d="M1020 708q3 7 3 16t-3 18q-3 4 -4.5 7t-4.5 6l-170 171q-13 13 -30 13t-30 -13t-13 -30t13 -30l98 -98h-239q-19 0 -31 -11.5t-12 -31.5q0 -19 12 -30.5t31 -11.5h239l-98 -98q-13 -13 -13 -30t13 -30q6 -7 13 -10t17 -3q9 0 16 3t14 10l170 170q3 4 6 7t3 6v0zM870 346
+q-28 3 -56.5 9.5t-54.5 15.5q-35 13 -71 4.5t-65 -34.5l-30 -30q-58 39 -108 87.5t-88 109.5l30 30q25 25 33.5 63t-3.5 73q-10 26 -16.5 54t-9.5 57q-6 48 -43 79.5t-85 31.5v0v0h-128h-6.5h-6.5q-25 -3 -47 -15.5t-38 -31.5t-23 -43.5t-7 -50.5q5 -51 16 -102
+q12 -51 29.5 -100t40.5 -96t50 -90q26 -40 56 -77q30 -38 64 -72t72 -64q37 -30 77 -56q43 -29 90 -52t96.5 -40t99.5 -28q51 -12 103 -16h6h6v0v0q26 0 50 9.5t40 28.5t27 41.5t11 48.5v128q0 48 -31.5 84.5t-79.5 43.5v0zM896 218v-128q0 -10 -3 -17t-10 -13
+q-6 -7 -13.5 -10t-20.5 -3q-46 5 -92 15q-45 10 -89 26t-87 36q-42 21 -82 47q-37 22 -70 49q-34 27 -64.5 58t-57.5 65t-51 71q-26 40 -46 82q-21 42 -36.5 86t-26.5 90q-10 45 -15 92q0 9 3 16l6 14q6 6 15 11.5t19 5.5h128v0v0q16 0 27.5 -11.5t15.5 -27.5
+q3 -32 12 -65.5t22 -62.5q3 -13 0.5 -25t-9.5 -22l-55 -51q-10 -9 -12.5 -23.5t3.5 -27.5q24 -43 54 -82q29 -39 63.5 -73.5t73.5 -64.5q39 -29 82 -53q13 -7 27.5 -5.5t23.5 13.5l56 56q10 9 22 12t25 -4q32 -12 64 -19.5t64 -14.5q13 -3 23.5 -14.5t10.5 -27.5v0v0v0z" />
+    <glyph glyph-name="uniE9A8" unicode="&#xe9a8;" 
+d="M1011 926q-13 13 -30 13t-30 -13l-226 -226v153q0 20 -11.5 31.5t-30.5 11.5q-20 0 -31.5 -11.5t-11.5 -31.5v-256q0 -3 0.5 -8.5t3.5 -8.5q3 -6 9 -12t13 -9q3 -3 8 -3.5t9 -0.5h256q19 0 30.5 11.5t11.5 30.5q0 20 -11.5 31.5t-30.5 11.5h-154l226 226q13 13 13 30
+t-13 30zM870 346q-28 3 -56.5 9.5t-54.5 15.5q-35 13 -71 4.5t-65 -34.5l-30 -30q-58 39 -108 87.5t-88 109.5l30 30q25 25 33.5 63t-3.5 73q-10 26 -16.5 54t-9.5 57q-6 48 -43 79.5t-85 31.5v0v0h-128h-6.5h-6.5q-25 -3 -47 -15.5t-38 -31.5t-23 -43.5t-7 -50.5
+q5 -51 16 -102q12 -51 29.5 -100t40.5 -96t50 -90q26 -40 56 -77q30 -38 64 -72t72 -64q37 -30 77 -56q43 -29 90 -52t96.5 -40t99.5 -28q51 -12 103 -16h6h6v0v0q26 0 50 9.5t40 28.5t27 41.5t11 48.5v128q0 48 -31.5 84.5t-79.5 43.5v0zM896 218v-128q0 -10 -3 -17
+t-10 -13q-6 -7 -13.5 -10t-20.5 -3q-46 5 -92 15q-45 10 -89 26t-87 36q-42 21 -82 47q-37 22 -70 49q-34 27 -64.5 58t-57.5 65t-51 71q-26 40 -46 82q-21 42 -36.5 86t-26.5 90q-10 45 -15 92q0 9 3 16l6 14q6 6 15 11.5t19 5.5h128v0v0q16 0 27.5 -11.5t15.5 -27.5
+q3 -32 12 -65.5t22 -62.5q3 -13 0.5 -25t-9.5 -22l-55 -51q-10 -9 -12.5 -23.5t3.5 -27.5q24 -43 54 -82q29 -39 63.5 -73.5t73.5 -64.5q39 -29 82 -53q13 -7 27.5 -5.5t23.5 13.5l56 56q10 9 22 12t25 -4q32 -12 64 -19.5t64 -14.5q13 -3 23.5 -14.5t10.5 -27.5v0v0v0z" />
+    <glyph glyph-name="uniE9A9" unicode="&#xe9a9;" 
+d="M913 768l98 98q13 13 13 30t-13 30t-30 13t-30 -13l-98 -98l-98 98q-13 13 -30 13t-30 -13q-12 -13 -12 -30t12 -30l99 -98l-99 -98q-12 -13 -12 -30t12 -30q7 -6 14 -9.5t16 -3.5q10 0 17 3.5t13 9.5l98 98l98 -98q7 -6 14 -9.5t16 -3.5q10 0 17 3.5t13 9.5q13 13 13 30
+t-13 30l-98 98v0zM870 346q-28 3 -56.5 9.5t-54.5 15.5q-35 13 -71 4.5t-65 -34.5l-30 -30q-58 39 -108 87.5t-88 109.5l30 30q25 25 33.5 63t-3.5 73q-10 26 -16.5 54t-9.5 57q-6 48 -43 79.5t-85 31.5v0v0h-128h-6.5h-6.5q-25 -3 -47 -15.5t-38 -31.5t-23 -43.5t-7 -50.5
+q5 -51 16 -102q12 -51 29.5 -100t40.5 -96t50 -90q26 -40 56 -77q30 -38 64 -72t72 -64q37 -30 77 -56q43 -29 90 -52t96.5 -40t99.5 -28q51 -12 103 -16h6h6v0v0q26 0 50 9.5t40 28.5t27 41.5t11 48.5v128q0 48 -31.5 84.5t-79.5 43.5v0zM896 218v-128q0 -10 -3 -17
+t-10 -13q-6 -7 -13.5 -10t-20.5 -3q-46 5 -92 15q-45 10 -89 26t-87 36q-42 21 -82 47q-37 22 -70 49q-34 27 -64.5 58t-57.5 65t-51 71q-26 40 -46 82q-21 42 -36.5 86t-26.5 90q-10 45 -15 92q0 9 3 16l6 14q6 6 15 11.5t19 5.5h128v0v0q16 0 27.5 -11.5t15.5 -27.5
+q3 -32 12 -65.5t22 -62.5q3 -13 0.5 -25t-9.5 -22l-55 -51q-10 -9 -12.5 -23.5t3.5 -27.5q24 -43 54 -82q29 -39 63.5 -73.5t73.5 -64.5q39 -29 82 -53q13 -7 27.5 -5.5t23.5 13.5l56 56q10 9 22 12t25 -4q32 -12 64 -19.5t64 -14.5q13 -3 23.5 -14.5t10.5 -27.5v0v0v0z" />
+    <glyph glyph-name="uniE9AA" unicode="&#xe9aa;" 
+d="M222 350q6 0 12 2.5t9 5.5q16 10 19.5 27t-6.5 33q-26 39 -46 80q-21 41 -36.5 85t-26.5 89q-10 45 -15 92q0 9 3 16l6 14q6 6 15 11.5t19 5.5h128q16 0 27.5 -11.5t15.5 -27.5q3 -32 12 -65.5t22 -62.5q3 -13 0.5 -25t-9.5 -22l-55 -51q-13 -13 -13 -30t13 -30
+q13 -12 30 -12t29 12l56 56q26 25 34.5 63t-4.5 73q-10 26 -16.5 54.5t-9.5 56.5q-9 45 -46.5 76t-85.5 31v0v0h-128h-6.5h-6.5q-25 -3 -47 -15.5t-38 -31.5t-23 -43.5t-7 -50.5q5 -51 16 -102q12 -51 29.5 -100t40.5 -96t50 -90q7 -6 18 -11.5t21 -5.5v0zM1011 926
+q-13 13 -30 13t-30 -13l-524 -525v0v0v0v0l-414 -414q-13 -13 -13 -30t13 -30q6 -6 13 -9t17 -3q9 0 16 3t14 9l264 265q29 -26 58.5 -48t61.5 -42q43 -28 90 -52q46 -23 95.5 -40t100.5 -28t102 -16h6h7q25 0 49 9.5t40 28.5t27.5 41.5t11.5 48.5v128q0 48 -31.5 84.5
+t-79.5 43.5q-29 3 -57.5 9.5t-53.5 15.5q-35 13 -71.5 4.5t-65.5 -33.5l-30 -30q-19 13 -38 28t-38 31l490 491q13 13 13 30t-13 30v0zM580 222q13 -7 27.5 -5.5t23.5 13.5l56 56q10 9 22 12t25 -4q32 -12 64 -19.5t64 -14.5q16 -3 27 -14.5t11 -27.5v-128q0 -10 -3 -17
+t-10 -13q-6 -7 -13.5 -10t-20.5 -3q-46 5 -92 15q-45 10 -89 26t-86 36q-43 21 -83 47q-28 16 -54 37t-48 44l60 59q25 -25 56 -47.5t63 -41.5v0z" />
+    <glyph glyph-name="uniE9AB" unicode="&#xe9ab;" 
+d="M1020 913q-3 6 -9 12t-13 9q-3 4 -8 4.5t-9 0.5h-256q-19 0 -30.5 -12t-11.5 -31t11.5 -31t30.5 -12h154l-226 -226q-13 -13 -13 -30t13 -30q6 -6 13 -9t17 -3q9 0 16 3t14 9l226 227v-154q0 -19 11.5 -31t30.5 -12q20 0 31.5 12t11.5 31v256q0 3 -0.5 8.5t-3.5 8.5v0z
+M870 346q-28 3 -56.5 9.5t-54.5 15.5q-35 13 -71 4.5t-65 -34.5l-30 -30q-58 39 -108 87.5t-88 109.5l30 30q25 25 33.5 63t-3.5 73q-10 26 -16.5 54t-9.5 57q-6 48 -43 79.5t-85 31.5v0v0h-128h-6.5h-6.5q-25 -3 -47 -15.5t-38 -31.5t-23 -43.5t-7 -50.5q5 -51 16 -102
+q12 -51 29.5 -100t40.5 -96t50 -90q26 -40 56 -77q30 -38 64 -72t72 -64q37 -30 77 -56q43 -29 90 -52t96.5 -40t99.5 -28q51 -12 103 -16h6h6v0v0q26 0 50 9.5t40 28.5t27 41.5t11 48.5v128q0 48 -31.5 84.5t-79.5 43.5v0zM896 218v-128q0 -10 -3 -17t-10 -13
+q-6 -7 -13.5 -10t-20.5 -3q-46 5 -92 15q-45 10 -89 26t-87 36q-42 21 -82 47q-37 22 -70 49q-34 27 -64.5 58t-57.5 65t-51 71q-26 40 -46 82q-21 42 -36.5 86t-26.5 90q-10 45 -15 92q0 9 3 16l6 14q6 6 15 11.5t19 5.5h128v0v0q16 0 27.5 -11.5t15.5 -27.5
+q3 -32 12 -65.5t22 -62.5q3 -13 0.5 -25t-9.5 -22l-55 -51q-10 -9 -12.5 -23.5t3.5 -27.5q24 -43 54 -82q29 -39 63.5 -73.5t73.5 -64.5q39 -29 82 -53q13 -7 27.5 -5.5t23.5 13.5l56 56q10 9 22 12t25 -4q32 -12 64 -19.5t64 -14.5q13 -3 23.5 -14.5t10.5 -27.5v0v0v0z" />
+    <glyph glyph-name="uniE9AC" unicode="&#xe9ac;" 
+d="M922 299q-16 6 -32.5 0t-23.5 -22q-30 -73 -85 -126q-55 -52 -123.5 -80t-144.5 -28q-77 -1 -150 30q-74 30 -127 85q-52 55 -79.5 123t-28.5 145q-1 76 30 150q29 67 81 119.5t119 81.5q16 6 22 22.5t0 32.5q-7 16 -23.5 24t-32.5 2q-41 -18 -78 -43
+q-37 -26 -67.5 -57.5t-55.5 -68.5q-24 -37 -42 -79q-38 -89 -37 -183q0 -93 34 -176.5t98 -150.5q65 -67 153 -104q44 -19 91 -29t92 -10q69 0 135 20q65 19 122 56t102 90t72 120q6 16 0.5 32.5t-21.5 23.5v0zM512 896q-19 0 -31 -11.5t-12 -31.5v-426q0 -20 12 -31.5
+t31 -11.5h427q19 0 30.5 11.5t11.5 31.5q0 97 -36 183q-37 85 -100.5 149t-149.5 100q-85 37 -183 37v0zM555 469v337q67 -8 126 -37q58 -29 103 -73.5t73 -102.5t35 -124h-337z" />
+    <glyph glyph-name="uniE9AD" unicode="&#xe9ad;" 
+d="M832 461l-597 384q-10 6 -21.5 6t-21.5 -6q-10 -3 -15.5 -12.5t-5.5 -21.5v-768q0 -13 5.5 -22.5t15.5 -16.5q6 -3 10.5 -3.5t10.5 -0.5q7 0 12.5 2.5t9.5 6.5l597 384q10 6 15.5 15t5.5 19q0 9 -5.5 20t-15.5 14v0zM256 119v615l478 -307l-478 -308v0z" />
+    <glyph glyph-name="uniE9AE" unicode="&#xe9ae;" 
+d="M512 896q-98 0 -183 -37q-86 -36 -149.5 -100t-100.5 -149q-36 -86 -36 -183q0 -98 36 -183q37 -86 100.5 -149.5t149.5 -100.5q85 -37 183 -37t183 37q86 37 149.5 100.5t100.5 149.5q36 85 36 183q0 97 -36 183q-37 85 -100.5 149t-149.5 100q-85 37 -183 37zM512 43
+q-80 0 -150 30t-122 82t-82 122t-30 150t30 150t82 122t122 82t150 30t150 -30t122 -82t82 -122t30 -150t-30 -150t-82 -122t-122 -82t-150 -30zM708 461l-256 170q-9 7 -21 7t-21 -7q-13 -3 -19.5 -12t-6.5 -22v-341q0 -13 6 -22.5t15 -15.5q7 -4 11 -4.5t11 -0.5
+q6 0 12.5 3t12.5 6l256 171q10 6 13.5 15t3.5 19q0 9 -5 18t-12 16v0zM469 337v179l137 -89l-137 -90v0z" />
+    <glyph glyph-name="uniE9AF" unicode="&#xe9af;" 
+d="M811 469h-256v256q0 20 -12 31.5t-31 11.5t-31 -11.5t-12 -31.5v-256h-256q-19 0 -30.5 -11.5t-11.5 -30.5q0 -20 11.5 -31.5t30.5 -11.5h256v-256q0 -19 12 -31t31 -12t31 12t12 31v256h256q19 0 30.5 11.5t11.5 31.5q0 19 -11.5 30.5t-30.5 11.5v0z" />
+    <glyph glyph-name="uniE9B0" unicode="&#xe9b0;" 
+d="M512 896q-98 0 -183 -37q-86 -36 -149.5 -100t-100.5 -149q-36 -86 -36 -183q0 -98 36 -183q37 -86 100.5 -149.5t149.5 -100.5q85 -37 183 -37t183 37q86 37 149.5 100.5t100.5 149.5q36 85 36 183q0 97 -36 183q-37 85 -100.5 149t-149.5 100q-85 37 -183 37zM512 43
+q-80 0 -150 30t-122 82t-82 122t-30 150t30 150t82 122t122 82t150 30t150 -30t122 -82t82 -122t30 -150t-30 -150t-82 -122t-122 -82t-150 -30zM683 469h-128v128q0 20 -12 31.5t-31 11.5t-31 -11.5t-12 -31.5v-128h-128q-19 0 -30.5 -11.5t-11.5 -30.5q0 -20 11.5 -31.5
+t30.5 -11.5h128v-128q0 -19 12 -31t31 -12t31 12t12 31v128h128q19 0 30.5 11.5t11.5 31.5q0 19 -11.5 30.5t-30.5 11.5v0z" />
+    <glyph glyph-name="uniE9B1" unicode="&#xe9b1;" 
+d="M811 853h-598q-54 0 -91 -36.5t-37 -91.5v-597q0 -54 37 -91t91 -37h598q54 0 91 37t37 91v597q0 55 -37 91.5t-91 36.5zM853 128q0 -19 -11.5 -31t-30.5 -12h-598q-19 0 -30.5 12t-11.5 31v597q0 20 11.5 31.5t30.5 11.5h598q19 0 30.5 -11.5t11.5 -31.5v-597zM683 469
+h-128v128q0 20 -12 31.5t-31 11.5t-31 -11.5t-12 -31.5v-128h-128q-19 0 -30.5 -11.5t-11.5 -30.5q0 -20 11.5 -31.5t30.5 -11.5h128v-128q0 -19 12 -31t31 -12t31 12t12 31v128h128q19 0 30.5 11.5t11.5 31.5q0 19 -11.5 30.5t-30.5 11.5v0z" />
+    <glyph glyph-name="uniE9B2" unicode="&#xe9b2;" 
+d="M853 853h-682q-55 0 -91.5 -36.5t-36.5 -91.5v-256q0 -97 36 -183q37 -85 100.5 -149t149.5 -100q85 -37 183 -37t183 37q86 36 149.5 100t100.5 149q36 86 36 183v256q0 55 -36.5 91.5t-91.5 36.5v0zM896 469q0 -80 -30 -150t-82 -122t-122 -82t-150 -30t-150 30
+t-122 82t-82 122t-30 150v256q0 20 11.5 31.5t31.5 11.5h682q20 0 31.5 -11.5t11.5 -31.5v-256v0zM653 542l-141 -141l-141 141q-13 13 -30 13t-30 -13q-12 -13 -12 -30t12 -30l171 -171q7 -6 15.5 -9t14.5 -3t14.5 3t15.5 9l171 171q12 13 12 30t-12 30q-13 13 -30 13
+t-30 -13v0z" />
+    <glyph glyph-name="uniE9B3" unicode="&#xe9b3;" 
+d="M815 687q-13 13 -30 13t-30 -13t-13 -30t13 -30q50 -49 75 -112q24 -63 24 -128.5t-24 -128.5q-25 -63 -75 -113q-49 -50 -112 -74q-63 -25 -128.5 -25t-128.5 25q-63 24 -113 74t-74 113q-25 63 -25 128.5t25 128.5q24 63 74 112q13 13 13 30t-13 30t-30 13t-30 -13
+q-64 -62 -96 -142q-32 -79 -32.5 -161t31.5 -161q31 -80 93 -142q32 -30 68 -53q36 -24 74 -39.5t79 -23.5q40 -8 82 -8t82 8q41 8 79 23.5t74 39.5q36 23 68 53q62 62 94 142q31 79 31 161t-31 161q-32 80 -94 142v0zM512 384q19 0 31 11.5t12 31.5v426q0 20 -12 31.5
+t-31 11.5t-31 -11.5t-12 -31.5v-426q0 -20 12 -31.5t31 -11.5z" />
+    <glyph glyph-name="uniE9B4" unicode="&#xe9b4;" 
+d="M853 597h-42v256q0 20 -12 31.5t-31 11.5h-512q-19 0 -31 -11.5t-12 -31.5v-256h-42q-55 0 -91.5 -36.5t-36.5 -91.5v-213q0 -54 36.5 -91t91.5 -37h42v-128q0 -19 12 -31t31 -12h512q19 0 31 12t12 31v128h42q55 0 91.5 37t36.5 91v213q0 55 -36.5 91.5t-91.5 36.5z
+M299 811h426v-214h-426v214zM725 43h-426v256h426v-256zM896 256q0 -19 -11.5 -31t-31.5 -12h-42v128q0 20 -12 31.5t-31 11.5h-512q-19 0 -31 -11.5t-12 -31.5v-128h-42q-20 0 -31.5 12t-11.5 31v213q0 20 11.5 31.5t31.5 11.5h682q20 0 31.5 -11.5t11.5 -31.5v-213v0z" />
+    <glyph glyph-name="uniE9B5" unicode="&#xe9b5;" 
+d="M512 555q-54 0 -91 -37t-37 -91q0 -55 37 -91.5t91 -36.5t91 36.5t37 91.5q0 54 -37 91t-91 37zM512 384q-19 0 -31 11.5t-12 31.5q0 19 12 30.5t31 11.5t31 -11.5t12 -30.5q0 -20 -12 -31.5t-31 -11.5zM721 636q-13 13 -30 13t-30 -13q-12 -13 -12 -30t12 -30
+q32 -30 48 -69q16 -40 16.5 -80.5t-14.5 -79.5t-45 -70q-4 -3 -7 -6l-6 -6q-7 -16 -1.5 -33t18.5 -23q3 -3 8.5 -3.5t8.5 -0.5q10 0 16.5 3t13.5 10q46 43 69 97q24 55 24.5 111.5t-22.5 111.5q-22 55 -67 98v0zM299 427q0 41 16 80.5t48 68.5v0v0q12 13 10.5 30t-10.5 30
+q-13 13 -30 13t-30 -13v0v0q-43 -43 -65 -99q-21 -55 -21 -112.5t21 -112.5q22 -55 65 -99q6 -6 14.5 -9t15.5 -3q6 0 14.5 3t15.5 9q12 13 12 30t-12 30q-32 32 -48 72t-16 82v0zM239 700q13 13 13 30t-13 29q-13 13 -30 13t-30 -13q-69 -68 -103 -155t-34 -177.5
+t34 -177.5q34 -86 103 -155q7 -7 15.5 -10t14.5 -3t15 3t15 10q13 13 13 30t-13 30q-56 56 -84 127t-28 145.5t28 145.5q28 72 84 128v0zM845 759q-13 13 -30 13t-30 -13q-13 -12 -13 -29t13 -30q56 -56 84 -127t28 -144.5t-28 -143.5q-28 -71 -84 -127q-13 -13 -13 -30
+t13 -30q6 -6 15 -9.5t15 -3.5t14.5 3.5t15.5 9.5q69 67 103 153t34 176t-34 177t-103 155v0z" />
+    <glyph glyph-name="uniE9B6" unicode="&#xe9b6;" 
+d="M43 469h256q19 0 30.5 12t11.5 31t-11.5 31t-30.5 12h-150l120 115q25 25 59 46.5t69 34.5q64 22 131.5 19t128.5 -32t107 -79t68 -117q6 -16 22.5 -24t32.5 -2q16 7 24 23.5t2 32.5q-14 40 -36 76q-21 35 -48.5 66t-61.5 56q-33 26 -72 45q-38 19 -78 30q-41 10 -82 12
+t-83 -4q-41 -7 -81 -21q-45 -19 -85.5 -44.5t-72.5 -57.5l-128 -120v158q0 19 -11.5 31t-30.5 12q-20 0 -31.5 -12t-11.5 -31v-256v-2v-2v-6v-3q0 -3 2 -4t2 -4l0.5 -2.5t4.5 -2.5v0v0q3 -3 6 -5.5t6 -2.5v0v0q7 -7 12.5 -8t9.5 -1v0zM1024 350v4v4q0 4 -2 5t-2 4l-0.5 2
+t-4.5 2v0v0q-3 3 -4 3.5t-4 0.5l-2 2.5l-2 2.5h-2.5h-2.5l-4.5 0.5t-7.5 3.5v0v0h-256q-20 0 -31.5 -11.5t-11.5 -31.5q0 -19 11.5 -30.5t31.5 -11.5h149l-120 -116q-49 -49 -112 -74t-128.5 -25t-128.5 25t-113 74q-25 26 -46.5 59.5t-34.5 68.5q-6 16 -22.5 24t-32.5 2
+t-24 -22.5t-2 -32.5q16 -45 41.5 -85.5t61.5 -72.5q32 -32 67 -56q36 -23 74.5 -38t78.5 -23q41 -7 83 -7q41 0 82 8q40 8 78 23.5t73 38.5t65 54l128 119v-158q0 -19 12 -30.5t31 -11.5t31 11.5t12 30.5v256q-7 4 -8 5t-1 4v0z" />
+    <glyph glyph-name="uniE9B7" unicode="&#xe9b7;" 
+d="M887 337q-16 6 -32.5 -2t-22.5 -24q-13 -35 -32.5 -68.5t-48.5 -59.5q-45 -48 -108.5 -73t-130.5 -25v0v0q-67 0 -129.5 26t-113.5 77l-120 111h150q19 0 30.5 11.5t11.5 30.5q0 20 -11.5 31.5t-30.5 11.5h-256v0v0q-4 0 -6.5 -0.5t-2.5 -3.5h-2h-2l-2 -0.5t-2 -4.5
+q-4 0 -6.5 -2.5t-2.5 -5.5v0v0l-0.5 -2t-3.5 -2q0 -4 -2 -5t-2 -4v-4v-4v-2.5v-2.5v-256q0 -19 11.5 -30.5t30.5 -11.5t31 11.5t12 30.5v158l124 -119q27 -31 60 -54t70.5 -38.5t79.5 -23.5q41 -8 84 -8v0v0q43 0 85 8q41 8 79.5 23.5t73.5 38.5t65 54q32 32 59 72.5
+t43 85.5q4 19 -5 35.5t-25 19.5v0zM1024 508v2v2v256q0 19 -11.5 31t-31.5 12q-19 0 -30.5 -12t-11.5 -31v-158l-124 120q-32 32 -72.5 59t-85.5 43q-42 13 -84 18q-42 6 -83.5 3.5t-82.5 -13.5q-40 -10 -78 -29q-39 -18 -72 -43q-34 -24 -61.5 -55t-48.5 -67
+q-22 -37 -36 -79q-6 -16 2 -32t24 -19q16 -6 32.5 2t22.5 24q22 64 68 114t107 82q61 29 128.5 32t131.5 -19q35 -13 69 -32.5t59 -48.5l120 -115h-150q-19 0 -30.5 -12t-11.5 -31t11.5 -31t30.5 -12h256q4 0 9 0.5t8 4.5v0v0q4 3 7 4t6 4v0v0l0.5 2t3.5 2q0 4 2.5 5t2.5 4
+q3 3 3.5 6.5t0.5 6.5v0z" />
+    <glyph glyph-name="uniE9B8" unicode="&#xe9b8;" 
+d="M128 427q19 0 31 11.5t12 30.5v86q0 54 36.5 91t91.5 37h495l-99 -98q-12 -13 -12 -30t12 -30q7 -7 14 -10t16 -3q10 0 17 3t13 10l171 170q3 4 5.5 7t2.5 6q4 7 4 16t-4 18q-3 4 -4 7t-4 6l-171 171q-13 13 -30 13t-30 -13q-12 -13 -12 -30t12 -30l99 -98h-495
+q-45 0 -84 -17q-39 -16 -68 -45t-45 -68q-17 -39 -17 -83v-86q0 -19 12 -30.5t31 -11.5v0zM896 427q-19 0 -31 -12t-12 -31v-85q0 -55 -36.5 -91.5t-91.5 -36.5h-495l99 98q12 13 12 30t-12 30q-13 12 -30 12t-30 -12l-171 -171q-3 -3 -5.5 -6.5t-2.5 -6.5q-4 -6 -4 -15
+t4 -19q3 -3 4 -6.5t4 -6.5l171 -171q6 -6 13 -9t17 -3q9 0 16 3t14 9q12 13 12 30t-12 30l-99 98h495q45 0 84 17q39 16 68 45t45 68q17 39 17 84v85q0 19 -12 31t-31 12v0z" />
+    <glyph glyph-name="uniE9B9" unicode="&#xe9b9;" 
+d="M486 764q-9 6 -23 4t-24 -9l-384 -298q-6 -7 -9 -16t-3 -18q0 -10 3.5 -19t13.5 -15l384 -299q6 -3 12.5 -6t12.5 -3q4 0 9 0.5t8 4.5q10 6 18 15.5t8 22.5v597q0 13 -6.5 22.5t-19.5 16.5v0zM427 213l-273 214l273 213v-427v0zM956 764q-10 6 -24 4t-23 -9l-384 -298
+q-7 -7 -10 -16t-3 -18q0 -10 3.5 -19t13.5 -15l384 -299q6 -3 12.5 -6t13.5 -3q3 0 8.5 0.5t8.5 4.5q9 6 17 15.5t8 22.5v597q0 13 -6 22.5t-19 16.5v0zM896 213l-273 214l273 213v-427v0z" />
+    <glyph glyph-name="uniE9BA" unicode="&#xe9ba;" 
+d="M913 567q-14 40 -36 76q-21 36 -48.5 67t-61.5 56q-33 25 -72 45q-38 19 -78 29q-41 11 -82 13t-83 -5q-41 -6 -81 -20q-45 -16 -85.5 -41t-72.5 -57l-128 -120v158q0 19 -11.5 31t-30.5 12q-20 0 -31.5 -12t-11.5 -31v-256v-2v-2v-6v-3q0 -3 2 -4t2 -4l0.5 -2.5
+t4.5 -2.5v0v0q3 -3 4 -3.5t4 -0.5l2 -2l2 -2h2.5h2.5h4h4v0v0h256q19 0 31 11.5t12 31.5q0 19 -7.5 26.5t-26.5 7.5h-150l120 115q25 25 59 46.5t69 34.5q64 22 131.5 19t128.5 -32t107 -79t68 -117q22 -64 19 -132t-32 -128q-29 -61 -79 -107t-117 -68q-67 -24 -135 -20
+q-67 5 -126 33t-104 78q-46 51 -70 118q-7 16 -23.5 24t-32.5 1q-16 -6 -24 -22.5t-1 -32.5q24 -66 65 -119q42 -52 95.5 -89.5t116.5 -57.5q62 -20 128 -20q35 0 70.5 6.5t70.5 19.5q83 28 145 85q63 58 98.5 131.5t41.5 157.5q5 84 -25 167z" />
+    <glyph glyph-name="uniE9BB" unicode="&#xe9bb;" 
+d="M1024 508v2v2v256q0 19 -11.5 31t-31.5 12q-19 0 -30.5 -12t-11.5 -31v-158l-124 120q-32 32 -72.5 59t-85.5 43q-85 29 -170 22q-85 -6 -159 -42t-131 -99q-57 -62 -86 -146q-29 -83 -23 -167t41 -158t97 -132t145 -89q35 -9 70.5 -15t70.5 -6q66 0 128 20
+q63 20 116 57.5t94 89.5q41 53 63 119q6 16 -2 32.5t-24 22.5q-16 7 -32.5 -1t-22.5 -24q-24 -67 -70 -117q-46 -49 -104.5 -77t-126.5 -33q-67 -5 -134 18q-67 24 -117 69q-49 46 -77 105t-33 126q-5 68 18 135q24 65 70 115q47 49 106.5 77t127.5 32q68 5 135 -19
+q35 -13 69 -33t59 -48l120 -111h-150q-19 0 -30.5 -12t-11.5 -31t11.5 -31t30.5 -12h256v0v0q4 0 6.5 0.5t2.5 4.5h2h2l2 2l2 2q4 0 6.5 2.5t2.5 5.5v0v0l0.5 2.5t3.5 2.5q0 3 2 4t2 4q7 0 8 3t1 6v0z" />
+    <glyph glyph-name="uniE9BC" unicode="&#xe9bc;" 
+d="M171 512q-20 0 -31.5 -11.5t-11.5 -31.5q0 -19 11.5 -30.5t31.5 -11.5q70 0 132 -27t108.5 -73.5t73.5 -108.5t27 -133q0 -19 11.5 -30.5t31.5 -11.5q19 0 30.5 11.5t11.5 30.5q0 88 -33 166q-34 78 -92 136t-136 91q-77 34 -165 34v0zM171 811q-20 0 -31.5 -12
+t-11.5 -31t11.5 -31t31.5 -12q132 0 249 -50t203.5 -137t136.5 -203q51 -117 51 -250q0 -19 11.5 -30.5t30.5 -11.5q20 0 31.5 11.5t11.5 30.5q0 151 -57 283t-155.5 230.5t-230.5 155.5t-282 57v0zM299 128q0 -35 -25 -60t-61 -25q-35 0 -60 25t-25 60t25 60t60 25
+q36 0 61 -25t25 -60z" />
+    <glyph glyph-name="uniE9BD" unicode="&#xe9bd;" 
+d="M926 627l-213 214q-7 6 -14 9t-16 3h-470q-54 0 -91 -36.5t-37 -91.5v-597q0 -54 37 -91t91 -37h598q54 0 91 37t37 91v469q0 10 -3.5 17t-9.5 13v0zM683 85h-342v256h342v-256zM853 128q0 -19 -11.5 -31t-30.5 -12h-43v299q0 19 -11.5 31t-31.5 12h-426q-20 0 -31.5 -12
+t-11.5 -31v-299h-43q-19 0 -30.5 12t-11.5 31v597q0 20 11.5 31.5t30.5 11.5h43v-171q0 -19 11.5 -30.5t31.5 -11.5h341q19 0 31 11.5t12 30.5q0 20 -12 31.5t-31 11.5h-299v128h325l187 -188v-452v0z" />
+    <glyph glyph-name="uniE9BE" unicode="&#xe9be;" 
+d="M883 738q13 13 13 30t-13 30t-30 13t-30 -13l-311 -312l-111 111q13 20 19.5 41.5t6.5 44.5q0 70 -50.5 120t-120.5 50t-120.5 -50t-50.5 -120q0 -71 50.5 -121t120.5 -50q22 0 44 6.5t41 19.5l111 -111l-111 -111q-19 13 -41 19t-44 6q-70 0 -120.5 -50t-50.5 -120
+q0 -71 50.5 -121t120.5 -50t120.5 50t50.5 121q0 22 -6.5 44t-19.5 41l482 482v0zM171 683q0 35 25 60t60 25t60 -25t25 -60q0 -20 -6 -33.5t-19 -26.5v0v0v0v0q-13 -13 -27 -19.5t-33 -6.5q-35 0 -60 25t-25 61v0zM256 85q-35 0 -60 25t-25 61q0 35 25 60t60 25
+q19 0 33 -6.5t27 -19.5v0v0v0v0q13 -12 19 -26t6 -33q0 -36 -25 -61t-60 -25zM649 350q-13 13 -30 13t-30 -13t-13 -30t13 -30l234 -235q7 -6 15.5 -9t14.5 -3q7 0 15.5 3t14.5 9q13 13 13 30t-13 30l-234 235v0z" />
+    <glyph glyph-name="uniE9BF" unicode="&#xe9bf;" 
+d="M926 73l-158 157q38 48 61.5 110t23.5 129q0 80 -30 150t-82 122t-122 82t-150 30t-150 -30t-122 -82t-82 -122t-30 -150t30 -150t82 -122t122 -82t150 -30q68 0 129.5 22t109.5 64l158 -158q7 -7 15.5 -10t14.5 -3t14.5 3t15.5 10q13 13 13 30t-13 30v0zM171 469
+q0 63 23 117q23 55 63.5 95t94.5 64q55 23 117 23q63 0 117 -23q55 -24 95 -64t64 -95q23 -54 23 -117q0 -60 -23.5 -115t-61.5 -94v0v0v0v0q-42 -41 -95.5 -63t-113.5 -22q-64 -2 -120 21q-55 22 -96 62t-64 95q-23 54 -23 116v0z" />
+    <glyph glyph-name="uniE9C0" unicode="&#xe9c0;" 
+d="M981 858q0 3 -0.5 6t-3.5 6v2.5v2.5q0 3 -2.5 6t-5.5 6q-4 4 -7 5t-6 4h-2.5h-2.5q-3 0 -6 0.5t-6 3.5v-3.5v-0.5q-4 0 -7 -0.5t-6 -3.5l-853 -299q-13 -3 -21.5 -12.5t-8.5 -25.5q0 -13 6 -25t19 -18l367 -162l162 -367q7 -13 16.5 -19.5t22.5 -6.5v0v0q13 0 24 8.5
+t14 21.5l299 854q6 3 7 6.5t1 10.5v-4v4v0zM777 751l-316 -316l-260 115l576 201v0zM636 115l-115 260l315 316l-200 -576v0z" />
+    <glyph glyph-name="uniE9C1" unicode="&#xe9c1;" 
+d="M853 896h-682q-55 0 -91.5 -37t-36.5 -91v-171q0 -54 36.5 -91t91.5 -37h682q55 0 91.5 37t36.5 91v171q0 54 -36.5 91t-91.5 37zM896 597q0 -19 -11.5 -30.5t-31.5 -11.5h-682q-20 0 -31.5 11.5t-11.5 30.5v171q0 19 11.5 31t31.5 12h682q20 0 31.5 -12t11.5 -31v-171z
+M853 384h-682q-55 0 -91.5 -37t-36.5 -91v-171q0 -54 36.5 -91t91.5 -37h682q55 0 91.5 37t36.5 91v171q0 54 -36.5 91t-91.5 37zM896 85q0 -19 -11.5 -30.5t-31.5 -11.5h-682q-20 0 -31.5 11.5t-11.5 30.5v171q0 19 11.5 31t31.5 12h682q20 0 31.5 -12t11.5 -31v-171z
+M226 713q-6 -7 -9.5 -14t-3.5 -16q0 -10 3.5 -17t9.5 -13q7 -7 13.5 -10t16.5 -3t16.5 3t13.5 10q6 6 9.5 13t3.5 17q0 9 -3.5 16t-9.5 14q-13 12 -30 12t-30 -12v0zM226 201q-6 -7 -9.5 -14t-3.5 -16q0 -10 3.5 -17t9.5 -13q7 -7 13.5 -10t16.5 -3t16.5 3t13.5 10
+q6 6 9.5 13t3.5 17q0 9 -3.5 16t-9.5 14q-13 12 -30 12t-30 -12v0z" />
+    <glyph glyph-name="uniE9C2" unicode="&#xe9c2;" 
+d="M512 597q-70 0 -120.5 -50t-50.5 -120q0 -71 50.5 -121t120.5 -50t120.5 50t50.5 121q0 70 -50.5 120t-120.5 50zM512 341q-35 0 -60 25t-25 61q0 35 25 60t60 25t60 -25t25 -60q0 -36 -25 -61t-60 -25zM866 282q3 6 10 11.5t20 5.5q54 0 91 36.5t37 91.5q0 54 -37 91
+t-91 37h-9q-6 0 -12 3t-9 9q0 4 -0.5 5t-3.5 4q-3 6 -2 15.5t10 18.5q39 39 39 91.5t-39 87.5v0v0v0q-19 20 -41.5 29.5t-47.5 9.5v0v0q-26 0 -50.5 -9.5t-43.5 -29.5q-6 -6 -15 -6.5t-15 2.5q-6 0 -11.5 8t-5.5 18q0 54 -37 91t-91 37t-91 -37t-37 -91v-9q0 -6 -3 -12
+t-10 -9q-3 0 -4 -0.5t-4 -3.5q-7 -4 -16 -1t-18 9q-39 38 -91.5 38t-88.5 -38q-38 -38 -37.5 -91.5t42.5 -91.5q6 -7 6.5 -16t-2.5 -19q-3 -6 -11.5 -11.5t-18.5 -5.5q-54 0 -91 -36.5t-37 -91.5q0 -54 37 -91t91 -37h9q9 0 15.5 -5t9.5 -12q3 -6 2 -15t-10 -19
+q-20 -19 -29.5 -41.5t-9.5 -47.5q0 -26 9.5 -48.5t29.5 -41.5v0v0q38 -38 91.5 -37.5t91.5 41.5q6 7 15.5 7.5t18.5 -2.5q10 -4 13.5 -11t3.5 -19q0 -55 37 -91.5t91 -36.5q55 0 91.5 36.5t36.5 91.5v8q0 10 5.5 16.5t11.5 9.5q7 3 16 2t18 -11q39 -38 91.5 -38t88.5 38
+q38 39 37.5 92t-42.5 92q-3 6 -5 14.5t1 15.5v0v0zM789 316q-12 -32 -6 -65.5t32 -62.5q6 -7 9.5 -14t3.5 -16q0 -10 -3.5 -17t-9.5 -13t-13 -9.5t-17 -3.5v0v0q-10 0 -17 4t-17 13q-26 26 -59 32t-65 -10q-32 -13 -50 -42t-18 -61v-8q0 -20 -12 -31.5t-31 -11.5t-30.5 11.5
+t-11.5 31.5v2v2q0 35 -20.5 62.5t-52.5 39.5q-10 7 -22 8t-25 1q-22 0 -43.5 -9t-37.5 -25q-13 -13 -30 -13t-30 13v0v0v0q-6 6 -9 13t-3 17q0 9 3.5 16.5t13.5 17.5q25 25 31 58t-10 65q-13 32 -41.5 50.5t-60.5 18.5h-9q-19 0 -31 11.5t-12 30.5q0 20 12 31.5t31 11.5h2h2
+q35 0 62.5 20.5t40.5 52.5q12 32 6 65.5t-32 62.5q-13 12 -13 29.5t13 29.5q13 13 30.5 12.5t33.5 -16.5q22 -22 54 -28.5t61 2.5q3 0 6.5 1t6.5 4q32 13 50 41.5t18 60.5v9q0 19 12 30.5t31 11.5t31 -12t12 -35q0 -35 18 -62t50 -40t65.5 -6.5t62.5 32.5q6 6 13 9t17 3v0
+q9 0 16 -3t14 -9v0v0q12 -13 11.5 -30.5t-16.5 -33.5q-22 -23 -28.5 -55t3.5 -61q0 -3 0.5 -6t3.5 -6q13 -32 41.5 -50.5t60.5 -18.5h9q19 0 31 -11.5t12 -30.5q0 -20 -12.5 -31.5t-34.5 -11.5q-32 0 -61 -18t-42 -50v0z" />
+    <glyph glyph-name="uniE9C3" unicode="&#xe9c3;" 
+d="M853 469q-19 0 -30.5 -11.5t-11.5 -30.5v-342q0 -19 -12 -30.5t-31 -11.5h-512q-19 0 -31 11.5t-12 30.5v342q0 19 -11.5 30.5t-30.5 11.5q-20 0 -31.5 -11.5t-11.5 -30.5v-342q0 -54 37 -91t91 -37h512q54 0 91 37t37 91v342q0 19 -11.5 30.5t-31.5 11.5v0zM371 653
+l98 98v-452q0 -20 12 -31.5t31 -11.5t31 11.5t12 31.5v452l98 -98q6 -7 13 -10t17 -3q9 0 16 3t14 10q12 13 12 30t-12 30l-171 170q-3 3 -6.5 6t-6.5 3q-6 3 -15 3t-19 -3q-3 -3 -6.5 -4.5t-6.5 -4.5l-171 -170q-12 -13 -12 -30t12 -30q13 -13 30 -13t30 13v0z" />
+    <glyph glyph-name="uniE9C4" unicode="&#xe9c4;" 
+d="M768 299q-35 0 -66 -14.5t-53 -37.5l-227 133q0 13 2.5 23.5t2.5 23.5q0 12 -3 23t-6 24l226 132q26 -23 57.5 -37t66.5 -14q70 0 120.5 50t50.5 120q0 71 -50.5 121t-120.5 50t-120.5 -50t-50.5 -121q0 -12 3 -23t6 -24l-231 -132q-22 23 -53 37t-66 14
+q-70 0 -120.5 -50t-50.5 -120q0 -71 50.5 -121t120.5 -50q35 0 66 14.5t53 36.5l227 -132q0 -13 -2.5 -23.5t-2.5 -23.5q0 -70 50.5 -120.5t120.5 -50.5t120.5 50.5t50.5 120.5t-50.5 120.5t-120.5 50.5v0zM768 811q35 0 60 -25t25 -61q0 -35 -25 -60t-60 -25t-60 25t-25 60
+q0 36 25 61t60 25zM256 341q-35 0 -60 25t-25 61q0 35 25 60t60 25t60 -25t25 -60q0 -36 -25 -61t-60 -25v0zM768 43q-35 0 -60 25t-25 60q0 13 3 23t9 20v0v0v0v0q10 19 30.5 30.5t42.5 11.5q35 0 60 -25t25 -60t-25 -60t-60 -25v0z" />
+    <glyph glyph-name="uniE9C5" unicode="&#xe9c5;" 
+d="M870 764l-341 128q-6 3 -14.5 3t-15.5 -3l-341 -128q-13 -3 -21.5 -14.5t-8.5 -24.5v-298q0 -104 55 -189q55 -84 122 -144.5t125 -94.5q59 -34 65 -37q3 -4 7 -4.5t10 -0.5t10 0.5t7 4.5q6 3 65 37q58 34 125 94.5t122 144.5q55 85 55 189v298q0 13 -8 24.5t-18 14.5v0z
+M811 427q0 -72 -37 -135q-37 -62 -87 -111t-100 -84q-49 -34 -75 -50q-26 14 -75 48q-50 33 -100 82.5t-87 112.5t-37 137v268l299 111l299 -111v-268v0z" />
+    <glyph glyph-name="uniE9C6" unicode="&#xe9c6;" 
+d="M393 764l119 42l299 -111v-268q0 -20 -3.5 -38.5t-9.5 -34.5q-7 -16 2 -32t28 -19h6h7q12 0 24 8.5t18 21.5q7 22 10.5 47t6.5 51v294q0 13 -8 24.5t-17 14.5l-342 128q-6 3 -14.5 3t-15.5 -3l-140 -47q-16 -7 -24 -23.5t-2 -32.5t23 -24t33 -1v0zM1011 -13l-938 939
+q-13 13 -30 13t-30 -13t-13 -30t13 -30l119 -119q0 -7 -2 -11t-2 -11v-298q0 -104 55 -189q55 -84 122 -144.5t125 -94.5q59 -34 65 -37q3 -4 7 -4.5t10 -0.5t10.5 0.5t10.5 4.5q61 32 116 73t106 89l201 -201q6 -6 14.5 -9.5t15.5 -3.5q6 0 14.5 3.5t14.5 9.5
+q10 16 9.5 33.5t-13.5 30.5v0zM512 47q-26 14 -75 48q-50 33 -100 82.5t-87 112.5t-37 137v239l478 -478q-38 -42 -83 -77t-96 -64v0z" />
+    <glyph glyph-name="uniE9C7" unicode="&#xe9c7;" 
+d="M934 700l-0.5 2t-3.5 2l-0.5 2t-3.5 2l-128 171q-3 10 -11.5 13.5t-18.5 3.5h-512q-10 0 -19 -3.5t-15 -13.5l-128 -171l-0.5 -2t-3.5 -2l-0.5 -2t-4.5 -2v-10.5v-6.5v-598q0 -54 37 -91t91 -37h598q54 0 91 37t37 91v598q0 3 -0.5 6.5t-4.5 10.5v0zM277 811h470l64 -86
+h-598l64 86v0zM811 43h-598q-19 0 -30.5 11.5t-11.5 30.5v555h682v-555q0 -19 -11.5 -30.5t-30.5 -11.5zM683 555q-20 0 -31.5 -12t-11.5 -31q0 -54 -37 -91t-91 -37t-91 37t-37 91q0 19 -11.5 31t-31.5 12q-19 0 -30.5 -12t-11.5 -31q0 -45 16 -84q17 -39 45.5 -67.5
+t67.5 -45.5q39 -16 84 -16t84 16q39 17 67.5 45.5t45.5 67.5q16 39 16 84q0 19 -11.5 31t-30.5 12v0z" />
+    <glyph glyph-name="uniE9C8" unicode="&#xe9c8;" 
+d="M469 43q0 -36 -25 -61t-60 -25t-60 25t-25 61q0 35 25 60t60 25t60 -25t25 -60v0zM939 43q0 -36 -25 -61t-61 -25q-35 0 -60 25t-25 61q0 35 25 60t60 25q36 0 61 -25t25 -60zM1015 708q-6 10 -15 13.5t-19 3.5h-691l-34 180q-3 16 -15 25t-28 9h-170q-20 0 -31.5 -12
+t-11.5 -31t11.5 -31t31.5 -12h136l34 -179v-2v-2l73 -354q9 -45 44 -74t80 -29h0.5h3.5h414q48 0 83 29t45 74l68 358q0 10 -1 19t-8 15v0zM870 333q-3 -16 -14.5 -25t-27.5 -9h-414q-16 0 -28 9t-15 25l-64 307h623l-60 -307v0z" />
+    <glyph glyph-name="uniE9C9" unicode="&#xe9c9;" 
+d="M934 828q-3 6 -9 12t-12 9q-3 3 -8.5 3.5t-8.5 0.5h-213q-20 0 -31.5 -11.5t-11.5 -30.5q0 -20 11.5 -31.5t31.5 -11.5h111l-653 -653q-13 -13 -13 -30t13 -30q6 -6 13 -9t17 -3q9 0 16 3t14 9l652 653v-111q0 -19 12 -30.5t31 -11.5t31 11.5t12 30.5v214q0 3 -0.5 8.5
+t-4.5 8.5v0zM896 299q-19 0 -31 -12t-12 -31v-111l-183 184q-13 12 -30 12t-30 -12q-13 -13 -13 -30t13 -30l184 -184h-111q-20 0 -31.5 -11.5t-11.5 -30.5q0 -20 11.5 -31.5t31.5 -11.5h213q3 0 8.5 0.5t8.5 3.5q6 3 12 9t9 13q4 3 4.5 8t0.5 9v213q0 19 -12 31t-31 12z
+M354 525q7 -7 13.5 -10t16.5 -3t16.5 3t13.5 10q13 13 13 30t-13 30l-213 213q-13 13 -30 13t-30 -13t-13 -30t13 -30l213 -213v0z" />
+    <glyph glyph-name="uniE9CA" unicode="&#xe9ca;" 
+d="M811 853h-598q-54 0 -91 -36.5t-37 -91.5v-597q0 -54 37 -91t91 -37h598q54 0 91 37t37 91v597q0 55 -37 91.5t-91 36.5zM171 128v597q0 20 11.5 31.5t30.5 11.5h128v-683h-128q-19 0 -30.5 12t-11.5 31zM853 128q0 -19 -11.5 -31t-30.5 -12h-384v683h384
+q19 0 30.5 -11.5t11.5 -31.5v-597z" />
+    <glyph glyph-name="uniE9CB" unicode="&#xe9cb;" 
+d="M828 806q-10 7 -24 5t-23 -9l-427 -341q-6 -7 -9.5 -16t-3.5 -18q0 -10 4 -19t13 -15l427 -342q6 -3 12.5 -5.5t13.5 -2.5q3 0 8.5 0.5t8.5 3.5q9 6 17 16t8 22v683q0 13 -6 22.5t-19 15.5v0zM768 175l-316 252l316 251v-503v0zM213 768q-19 0 -30.5 -11.5t-11.5 -31.5
+v-597q0 -19 11.5 -31t30.5 -12q20 0 31.5 12t11.5 31v597q0 20 -11.5 31.5t-31.5 11.5v0z" />
+    <glyph glyph-name="uniE9CC" unicode="&#xe9cc;" 
+d="M239 802q-10 7 -21.5 9t-21.5 -5q-13 -6 -19 -15.5t-6 -22.5v-683q0 -12 6 -22t19 -16q3 -3 8.5 -3.5t8.5 -0.5q7 0 13.5 2.5t12.5 5.5l427 342q6 6 11.5 15t5.5 19q0 9 -4 18t-13 16l-427 341v0zM256 175v503l316 -251l-316 -252v0zM811 768q-20 0 -31.5 -11.5
+t-11.5 -31.5v-597q0 -19 11.5 -31t31.5 -12q19 0 30.5 12t11.5 31v597q0 20 -11.5 31.5t-30.5 11.5v0z" />
+    <glyph glyph-name="uniE9CD" unicode="&#xe9cd;" 
+d="M981 567q-20 68 -43 121t-49.5 94t-57.5 70q-30 29 -67 48q-34 19 -74 29q-41 10 -89 9.5t-105 -11.5q-58 -10 -125 -31q-129 -38 -212 -91q-82 -52 -121.5 -124.5t-37.5 -169.5q3 -97 43 -225q28 -96 64 -166t82 -115.5t104 -67.5q57 -22 129 -22q52 0 108 9.5t123 28.5
+q128 40 210 93t122 126t38 170t-42 225zM627 38q-112 -33 -192 -37q-80 -3 -138 28.5t-99 100.5t-74 181q-34 111 -37 191q-4 80 27.5 138t101.5 100q69 41 181 75q64 19 115.5 28.5t93.5 9.5q35 0 66.5 -6t57.5 -19q27 -15 51 -39t45.5 -59t41.5 -80q19 -46 37 -104v0v0
+q32 -112 34 -193q3 -80 -29 -139t-101 -100q-70 -42 -182 -76v0zM755 444l-81 -30l-43 119l82 30q16 7 24 21.5t1 34.5q-6 16 -21 24t-34 1l-81 -30l-30 81q-7 16 -22 24t-34 2q-16 -6 -24 -21t-1 -34l30 -81l-120 -43l-26 85q-6 16 -22.5 24t-32.5 2q-16 -3 -24 -19t-2 -32
+l30 -81l-81 -30q-16 -7 -24 -22t-1 -34q3 -13 14 -21.5t24 -8.5q3 0 6.5 0.5t6.5 4.5l81 29l43 -119l-82 -30q-16 -6 -24 -21t-1 -34q3 -13 14 -21.5t24 -8.5q3 0 6.5 0.5t6.5 3.5l81 30l30 -81q3 -13 14.5 -21.5t23.5 -8.5q4 0 7 0.5t6 3.5q16 7 24 22t2 34l-30 81l119 42
+l30 -81q3 -12 14.5 -20.5t24.5 -8.5q3 0 6 0.5t6 3.5q16 6 24 21t2 34l-30 81l81 30q16 7 24 21.5t2 34.5q3 9 -12 16t-31 1v0zM474 346l-43 119l119 43l43 -120z" />
+    <glyph glyph-name="uniE9CE" unicode="&#xe9ce;" 
+d="M845 94q32 32 57 69q25 38 43 80t27 88t9 96q0 97 -36 183q-37 85 -100.5 149t-149.5 100q-85 37 -183 37q-48 0 -94 -9q-46 -10 -88.5 -27.5t-80.5 -42.5q-38 -26 -70 -58v0v0v0v0q-32 -32 -57 -70t-43 -80t-27 -88t-9 -94q0 -98 36 -183q37 -86 100.5 -149.5
+t149.5 -100.5q85 -37 183 -37q48 0 94 10q46 9 88.5 26.5t80.5 43.5q38 25 70 57v0v0v0v0v0zM896 427q0 -68 -22 -129.5t-63 -109.5l-538 537q48 42 110 64t129 22q80 0 150 -30t122 -82t82 -122t30 -150zM128 427q0 67 22 129t63 110l538 -538q-48 -38 -110 -61.5
+t-129 -23.5q-80 0 -150 30t-122 82t-82 122t-30 150z" />
+    <glyph glyph-name="uniE9CF" unicode="&#xe9cf;" 
+d="M171 469q19 0 30.5 12t11.5 31v299q0 19 -11.5 30.5t-30.5 11.5q-20 0 -31.5 -11.5t-11.5 -30.5v-299q0 -19 11.5 -31t31.5 -12v0zM512 469q-19 0 -31 -11.5t-12 -30.5v-384q0 -20 12 -31.5t31 -11.5t31 11.5t12 31.5v384q0 19 -12 30.5t-31 11.5v0zM853 384
+q20 0 31.5 11.5t11.5 31.5v384q0 19 -11.5 30.5t-31.5 11.5q-19 0 -30.5 -11.5t-11.5 -30.5v-384q0 -20 11.5 -31.5t30.5 -11.5zM299 384h-256q-20 0 -31.5 -11.5t-11.5 -31.5q0 -19 11.5 -30.5t31.5 -11.5h85v-256q0 -20 11.5 -31.5t31.5 -11.5q19 0 30.5 11.5t11.5 31.5
+v256h86q19 0 30.5 11.5t11.5 30.5q0 20 -11.5 31.5t-30.5 11.5v0zM640 640h-85v171q0 19 -12 30.5t-31 11.5t-31 -11.5t-12 -30.5v-171h-85q-19 0 -31 -11.5t-12 -31.5q0 -19 12 -30.5t31 -11.5h256q19 0 31 11.5t12 30.5q0 20 -12 31.5t-31 11.5v0zM981 299h-256
+q-19 0 -30.5 -12t-11.5 -31t11.5 -31t30.5 -12h86v-170q0 -20 11.5 -31.5t30.5 -11.5q20 0 31.5 11.5t11.5 31.5v170h85q20 0 31.5 12t11.5 31t-11.5 31t-31.5 12z" />
+    <glyph glyph-name="uniE9D0" unicode="&#xe9d0;" 
+d="M725 896h-426q-55 0 -91.5 -37t-36.5 -91v-683q0 -54 36.5 -91t91.5 -37h426q55 0 91.5 37t36.5 91v683q0 54 -36.5 91t-91.5 37v0zM768 85q0 -19 -11.5 -30.5t-31.5 -11.5h-426q-20 0 -31.5 11.5t-11.5 30.5v683q0 19 11.5 31t31.5 12h426q20 0 31.5 -12t11.5 -31v-683z
+M482 201q-6 -7 -9.5 -14t-3.5 -16q0 -10 3.5 -17t9.5 -13q7 -7 13.5 -10t16.5 -3t16.5 3t13.5 10q6 6 9.5 14.5t3.5 15.5q0 6 -3.5 14.5t-9.5 15.5q-13 12 -30 12t-30 -12v0z" />
+    <glyph glyph-name="uniE9D1" unicode="&#xe9d1;" 
+d="M768 896h-512q-54 0 -91 -37t-37 -91v-683q0 -54 37 -91t91 -37h512q54 0 91 37t37 91v683q0 54 -37 91t-91 37v0zM811 85q0 -19 -12 -30.5t-31 -11.5h-512q-19 0 -31 11.5t-12 30.5v683q0 19 12 31t31 12h512q19 0 31 -12t12 -31v-683zM512 555q-45 0 -84 -17
+q-39 -16 -67.5 -45t-45.5 -68q-16 -39 -16 -84q0 -44 16 -83q17 -39 45.5 -68t67.5 -45q39 -17 84 -17t84 17q39 16 67.5 45t45.5 68q16 39 16 83q0 45 -16 84q-17 39 -45.5 68t-67.5 45q-39 17 -84 17zM512 213q-54 0 -91 37t-37 91q0 55 37 91.5t91 36.5t91 -36.5
+t37 -91.5q0 -54 -37 -91t-91 -37zM512 640q10 0 16.5 3t13.5 10q6 6 9.5 13t3.5 17q0 9 -3.5 16t-9.5 14q-13 12 -31.5 12t-28.5 -12q-6 -7 -9.5 -14t-3.5 -16q0 -10 3.5 -17t9.5 -13q7 -7 13.5 -10t16.5 -3v0z" />
+    <glyph glyph-name="uniE9D2" unicode="&#xe9d2;" 
+d="M811 853h-598q-54 0 -91 -36.5t-37 -91.5v-597q0 -54 37 -91t91 -37h598q54 0 91 37t37 91v597q0 55 -37 91.5t-91 36.5zM853 128q0 -19 -11.5 -31t-30.5 -12h-598q-19 0 -30.5 12t-11.5 31v597q0 20 11.5 31.5t30.5 11.5h598q19 0 30.5 -11.5t11.5 -31.5v-597z" />
+    <glyph glyph-name="uniE9D3" unicode="&#xe9d3;" 
+d="M981 555q-3 12 -12 19t-22 11l-273 38l-124 247q-9 23 -38 23t-38 -23l-124 -243l-269 -42q-13 0 -22.5 -9t-15.5 -21q-4 -13 -0.5 -25t12.5 -18l197 -192l-47 -273q-3 -13 2 -24.5t15 -18.5q9 -6 21.5 -8t25.5 4l243 128l243 -128h11h11q6 0 12.5 2.5t12.5 6.5
+q10 6 15 17.5t2 24.5l-47 273l197 192q9 7 12.5 18t-0.5 21v0zM695 367q-6 -10 -9 -19.5t-3 -18.5l34 -210l-188 99q-10 6 -19.5 6t-18.5 -6l-188 -99l38 210q0 9 -3 18.5t-9 19.5l-154 145l209 30q10 0 19 7.5t15 13.5l94 192l94 -188q3 -9 12 -15t22 -6l209 -30l-154 -149
+v0z" />
+    <glyph glyph-name="uniE9D4" unicode="&#xe9d4;" 
+d="M512 896q-98 0 -183 -37q-86 -36 -149.5 -100t-100.5 -149q-36 -86 -36 -183q0 -98 36 -183q37 -86 100.5 -149.5t149.5 -100.5q85 -37 183 -37t183 37q86 37 149.5 100.5t100.5 149.5q36 85 36 183q0 97 -36 183q-37 85 -100.5 149t-149.5 100q-85 37 -183 37zM512 43
+q-80 0 -150 30t-122 82t-82 122t-30 150t30 150t82 122t122 82t150 30t150 -30t122 -82t82 -122t30 -150t-30 -150t-82 -122t-122 -82t-150 -30zM640 597h-256q-19 0 -31 -11.5t-12 -30.5v-256q0 -20 12 -31.5t31 -11.5h256q19 0 31 11.5t12 31.5v256q0 19 -12 30.5
+t-31 11.5zM597 341h-170v171h170v-171z" />
+    <glyph glyph-name="uniE9D5" unicode="&#xe9d5;" 
+d="M512 683q-53 0 -99 -21q-47 -20 -82 -54.5t-55 -81.5t-20 -99q0 -53 20 -100q20 -46 55 -81t82 -55q46 -20 99 -20t99 20q47 20 82 55t55 81q20 47 20 100q0 52 -20 99t-55 81.5t-82 54.5q-46 21 -99 21zM512 256q-70 0 -120.5 50t-50.5 121q0 70 50.5 120t120.5 50
+t120.5 -50t50.5 -120q0 -71 -50.5 -121t-120.5 -50zM512 768q19 0 31 11.5t12 31.5v85q0 19 -12 31t-31 12t-31 -12t-12 -31v-85q0 -20 12 -31.5t31 -11.5zM512 85q-19 0 -31 -11.5t-12 -30.5v-86q0 -19 12 -30.5t31 -11.5t31 11.5t12 30.5v86q0 19 -12 30.5t-31 11.5z
+M209 670q6 -7 15 -10t15 -3t14.5 3t15.5 10q13 13 13 30t-13 30l-60 59q-13 13 -30 13t-30 -13q-12 -12 -12 -29.5t12 -29.5l60 -60v0zM815 183q-13 13 -30 13t-30 -13q-13 -12 -13 -29t13 -30l60 -60q6 -6 14.5 -9.5t15.5 -3.5q6 0 14.5 3.5t15.5 9.5q12 13 12 30t-12 30
+l-60 59v0zM171 427q0 19 -12 30.5t-31 11.5h-85q-20 0 -31.5 -11.5t-11.5 -30.5q0 -20 11.5 -31.5t31.5 -11.5h85q19 0 31 11.5t12 31.5zM981 469h-85q-19 0 -31 -11.5t-12 -30.5q0 -20 12 -31.5t31 -11.5h85q20 0 31.5 11.5t11.5 31.5q0 19 -11.5 30.5t-31.5 11.5v0z
+M209 183l-60 -59q-12 -13 -12 -30t12 -30q7 -6 15.5 -9.5t14.5 -3.5q7 0 15.5 3.5t14.5 9.5l60 60q13 13 13 30t-13 29q-13 13 -30 13t-30 -13v0zM785 657q10 0 17 3t13 10l60 60q12 12 12 29.5t-12 29.5q-13 13 -30 13t-30 -13l-60 -59q-13 -13 -13 -30t13 -30
+q3 -7 11.5 -10t18.5 -3v0z" />
+    <glyph glyph-name="uniE9D6" unicode="&#xe9d6;" 
+d="M512 427q-53 0 -99 -21q-47 -20 -82 -54.5t-55 -81.5t-20 -99q0 -20 11.5 -31.5t31.5 -11.5q19 0 30.5 11.5t11.5 31.5q0 70 50.5 120t120.5 50t120.5 -50t50.5 -120q0 -20 11.5 -31.5t30.5 -11.5q20 0 31.5 11.5t11.5 31.5q0 52 -20 99t-55 81.5t-82 54.5q-46 21 -99 21
+v0zM209 414q6 -7 15 -10t15 -3t14.5 3t15.5 10q13 13 13 30t-13 30l-60 59q-13 13 -30 13t-30 -13q-12 -12 -12 -29.5t12 -29.5l60 -60v0zM43 128h85q19 0 31 11.5t12 31.5q0 19 -12 30.5t-31 11.5h-85q-20 0 -31.5 -11.5t-11.5 -30.5q0 -20 11.5 -31.5t31.5 -11.5zM853 171
+q0 -20 12 -31.5t31 -11.5h85q20 0 31.5 11.5t11.5 31.5q0 19 -11.5 30.5t-31.5 11.5h-85q-19 0 -31 -11.5t-12 -30.5zM785 401q10 0 17 3t13 10l60 60q12 12 12 29.5t-12 29.5q-13 13 -30 13t-30 -13l-60 -59q-13 -13 -13 -30t13 -30q3 -7 11.5 -10t18.5 -3v0zM981 43h-938
+q-20 0 -31.5 -12t-11.5 -31t11.5 -31t31.5 -12h938q20 0 31.5 12t11.5 31t-11.5 31t-31.5 12zM371 653l98 98v-196q0 -20 12 -31.5t31 -11.5t31 11.5t12 31.5v196l98 -98q6 -7 13 -10t17 -3q9 0 16 3t14 10q12 13 12 30t-12 30l-171 170q-3 3 -6.5 6t-6.5 3q-6 3 -15 3
+t-19 -3q-3 -3 -6.5 -4.5t-6.5 -4.5l-171 -170q-12 -13 -12 -30t12 -30q13 -13 30 -13t30 13v0z" />
+    <glyph glyph-name="uniE9D7" unicode="&#xe9d7;" 
+d="M512 427q-53 0 -99 -21q-47 -20 -82 -54.5t-55 -81.5t-20 -99q0 -20 11.5 -31.5t31.5 -11.5q19 0 30.5 11.5t11.5 31.5q0 70 50.5 120t120.5 50t120.5 -50t50.5 -120q0 -20 11.5 -31.5t30.5 -11.5q20 0 31.5 11.5t11.5 31.5q0 52 -20 99t-55 81.5t-82 54.5q-46 21 -99 21
+v0zM209 414q6 -7 15 -10t15 -3t14.5 3t15.5 10q13 13 13 30t-13 30l-60 59q-13 13 -30 13t-30 -13q-12 -12 -12 -29.5t12 -29.5l60 -60v0zM43 128h85q19 0 31 11.5t12 31.5q0 19 -12 30.5t-31 11.5h-85q-20 0 -31.5 -11.5t-11.5 -30.5q0 -20 11.5 -31.5t31.5 -11.5zM853 171
+q0 -20 12 -31.5t31 -11.5h85q20 0 31.5 11.5t11.5 31.5q0 19 -11.5 30.5t-31.5 11.5h-85q-19 0 -31 -11.5t-12 -30.5zM785 401q10 0 17 3t13 10l60 60q12 12 12 29.5t-12 29.5q-13 13 -30 13t-30 -13l-60 -59q-13 -13 -13 -30t13 -30q3 -7 11.5 -10t18.5 -3v0zM981 43h-938
+q-20 0 -31.5 -12t-11.5 -31t11.5 -31t31.5 -12h938q20 0 31.5 12t11.5 31t-11.5 31t-31.5 12zM482 525q3 -3 6.5 -6t6.5 -3q3 -3 8.5 -3.5t8.5 -0.5t8.5 0.5t8.5 3.5t6.5 4.5t6.5 4.5l171 170q12 13 12 30t-12 30q-13 13 -30 13t-30 -13l-98 -98v196q0 20 -12 31.5t-31 11.5
+t-31 -11.5t-12 -31.5v-196l-98 98q-13 13 -30 13t-30 -13q-12 -13 -12 -30t12 -30l171 -170v0z" />
+    <glyph glyph-name="uniE9D8" unicode="&#xe9d8;" 
+d="M768 896h-512q-54 0 -91 -37t-37 -91v-683q0 -54 37 -91t91 -37h512q54 0 91 37t37 91v683q0 54 -37 91t-91 37v0zM811 85q0 -19 -12 -30.5t-31 -11.5h-512q-19 0 -31 11.5t-12 30.5v683q0 19 12 31t31 12h512q19 0 31 -12t12 -31v-683zM482 201q-6 -7 -9.5 -14t-3.5 -16
+q0 -10 3.5 -17t9.5 -13q7 -7 13.5 -10t16.5 -3t16.5 3t13.5 10q6 6 9.5 14.5t3.5 15.5q0 6 -3.5 14.5t-9.5 15.5q-13 12 -30 12t-30 -12v0z" />
+    <glyph glyph-name="uniE9D9" unicode="&#xe9d9;" 
+d="M909 516l-367 367q-7 7 -13.5 10t-16.5 3h-427q-19 0 -30.5 -11.5t-11.5 -31.5v-426q0 -10 3 -17t9 -13l367 -367q20 -19 44 -29t46 -10q26 0 48 10t42 29l307 307v0v0q38 38 38 89.5t-38 89.5v0zM849 397l-307 -307q-13 -13 -30 -13t-30 13l-354 354v367h367l354 -354
+q13 -13 13 -30t-13 -30zM269 670q-7 -7 -10 -13.5t-3 -16.5t3 -16.5t10 -13.5q6 -6 13 -9.5t17 -3.5q9 0 16 3.5t14 9.5q6 7 9 13.5t3 16.5t-3 16.5t-9 13.5q-13 13 -30 13t-30 -13v0z" />
+    <glyph glyph-name="uniE9DA" unicode="&#xe9da;" 
+d="M512 896q-98 0 -183 -37q-86 -36 -149.5 -100t-100.5 -149q-36 -86 -36 -183q0 -98 36 -183q37 -86 100.5 -149.5t149.5 -100.5q85 -37 183 -37t183 37q86 37 149.5 100.5t100.5 149.5q36 85 36 183q0 97 -36 183q-37 85 -100.5 149t-149.5 100q-85 37 -183 37zM512 43
+q-80 0 -150 30t-122 82t-82 122t-30 150t30 150t82 122t122 82t150 30t150 -30t122 -82t82 -122t30 -150t-30 -150t-82 -122t-122 -82t-150 -30zM512 725q-62 0 -117 -23q-54 -23 -94.5 -63.5t-63.5 -94.5q-24 -55 -24 -117q0 -63 24 -117q23 -55 63.5 -95t94.5 -64
+q55 -23 117 -23t117 23q54 24 94.5 64t63.5 95q24 54 24 117q0 62 -24 117q-23 54 -63.5 94.5t-94.5 63.5q-55 23 -117 23v0zM512 213q-45 0 -84 17q-39 16 -67.5 45t-45.5 68q-16 39 -16 84q0 44 16 83q17 39 45.5 68t67.5 45q39 17 84 17t84 -17q39 -16 67.5 -45t45.5 -68
+q16 -39 16 -83q0 -45 -16 -84q-17 -39 -45.5 -68t-67.5 -45q-39 -17 -84 -17zM512 555q-54 0 -91 -37t-37 -91q0 -55 37 -91.5t91 -36.5t91 36.5t37 91.5q0 54 -37 91t-91 37zM512 384q-19 0 -31 11.5t-12 31.5q0 19 12 30.5t31 11.5t31 -11.5t12 -30.5q0 -20 -12 -31.5
+t-31 -11.5z" />
+    <glyph glyph-name="uniE9DB" unicode="&#xe9db;" 
+d="M457 499l-256 256q-13 13 -30 13t-30 -13t-13 -30t13 -30l226 -226l-226 -226q-13 -13 -13 -30t13 -30q6 -6 13 -9t17 -3q9 0 16 3t14 9l256 256q12 13 12 30t-12 30v0zM853 171h-341q-19 0 -31 -12t-12 -31t12 -31t31 -12h341q20 0 31.5 12t11.5 31t-11.5 31t-31.5 12v0
+z" />
+    <glyph glyph-name="uniE9DC" unicode="&#xe9dc;" 
+d="M687 282q-10 16 -22 27t-25 24v456q0 61 -44.5 105.5t-104.5 44.5q-61 0 -105.5 -44.5t-44.5 -105.5v-460q-35 -29 -56 -67q-22 -38 -28 -79.5t3 -84.5t34 -81q26 -38 65.5 -65t84.5 -33q13 -3 23.5 -3.5t23.5 -0.5q35 0 67 9.5t61 28.5q41 27 67 66q26 40 35.5 84
+t0.5 91q-8 46 -35 88v0zM572 26q-51 -36 -112.5 -22.5t-96.5 64.5q-16 26 -22 54t0 57q7 29 23 53.5t41 40.5q10 6 13.5 15.5t3.5 18.5v482q4 26 23.5 45t45.5 19q25 0 44.5 -19t19.5 -45v-482q0 -9 5 -18.5t12 -15.5q13 -10 23 -20t19 -23q36 -51 22.5 -110t-64.5 -94v0z
+" />
+    <glyph glyph-name="uniE9DD" unicode="&#xe9dd;" 
+d="M981 772q-6 55 -46 89.5t-94 34.5h-1h-4h-593q-48 0 -83 -31t-45 -76l-60 -384q-6 -51 25 -94.5t82 -50.5h11h10h201v-128q0 -70 50 -120t121 -50q12 0 23.5 6t14.5 19l158 359h85q55 0 95 34.5t46 88.5v2.5v2.5v294q3 0 3.5 2l0.5 2v0zM683 393l-154 -346q-26 10 -43 31
+t-17 50v171q0 19 -11.5 30.5t-30.5 11.5h-244h-2.5h-5.5q-16 4 -26.5 17.5t-7.5 29.5l60 384q3 16 14.5 27.5t27.5 11.5h440v-418v0zM896 474q-3 -20 -19.5 -33.5t-35.5 -13.5h-73v384h73q19 3 35.5 -11t19.5 -36v-290v0z" />
+    <glyph glyph-name="uniE9DE" unicode="&#xe9de;" 
+d="M943 546q-16 19 -38 33.5t-47 17.5h-11h-11h-196v128q0 71 -50 121t-121 50q-12 0 -23.5 -6.5t-14.5 -19.5l-158 -358h-102q-55 0 -91.5 -37t-36.5 -91v-299q0 -54 36.5 -91t91.5 -37h610v0v0q48 0 83 31t45 76l60 384q3 26 -3.5 51t-22.5 47v0zM256 43h-85
+q-20 0 -31.5 11.5t-11.5 30.5v299q0 19 11.5 31t31.5 12h85v-384v0zM823 77q-3 -16 -14.5 -25t-27.5 -9v0v0h-440v418l154 345q26 -9 43 -30.5t17 -50.5v-170q0 -20 11.5 -31.5t30.5 -11.5h244h4h4q10 0 17 -5.5t13 -11.5t7 -15t1 -15l-64 -388v0z" />
+    <glyph glyph-name="uniE9DF" unicode="&#xe9df;" 
+d="M683 768h-342q-70 0 -132 -27t-108.5 -73.5t-73.5 -108.5t-27 -132q0 -71 27 -133t73.5 -108.5t108.5 -73.5t132 -27h342q70 0 132 27t108.5 73.5t73.5 108.5t27 133q0 70 -27 132t-73.5 108.5t-108.5 73.5t-132 27v0zM683 171h-342q-52 0 -99 20t-81.5 55t-54.5 81
+q-21 47 -21 100q0 52 21 99q20 47 54.5 81.5t81.5 54.5q47 21 99 21h342q52 0 99 -21q47 -20 81.5 -54.5t54.5 -81.5q21 -47 21 -99q0 -53 -21 -100q-20 -46 -54.5 -81t-81.5 -55t-99 -20zM341 597q-70 0 -120 -50t-50 -120q0 -71 50 -121t120 -50q71 0 121 50t50 121
+q0 70 -50 120t-121 50v0zM341 341q-35 0 -60 25t-25 61q0 35 25 60t60 25q36 0 61 -25t25 -60q0 -36 -25 -61t-61 -25v0z" />
+    <glyph glyph-name="uniE9E0" unicode="&#xe9e0;" 
+d="M683 768h-342q-70 0 -132 -27t-108.5 -73.5t-73.5 -108.5t-27 -132q0 -71 27 -133t73.5 -108.5t108.5 -73.5t132 -27h342q70 0 132 27t108.5 73.5t73.5 108.5t27 133q0 70 -27 132t-73.5 108.5t-108.5 73.5t-132 27v0zM683 171h-342q-52 0 -99 20t-81.5 55t-54.5 81
+q-21 47 -21 100q0 52 21 99q20 47 54.5 81.5t81.5 54.5q47 21 99 21h342q52 0 99 -21q47 -20 81.5 -54.5t54.5 -81.5q21 -47 21 -99q0 -53 -21 -100q-20 -46 -54.5 -81t-81.5 -55t-99 -20zM683 597q-71 0 -121 -50t-50 -120q0 -71 50 -121t121 -50q70 0 120 50t50 121
+q0 70 -50 120t-120 50zM683 341q-36 0 -61 25t-25 61q0 35 25 60t61 25q35 0 60 -25t25 -60q0 -36 -25 -61t-60 -25z" />
+    <glyph glyph-name="uniE9E1" unicode="&#xe9e1;" 
+d="M896 725h-171v43q0 54 -36.5 91t-91.5 37h-170q-55 0 -91.5 -37t-36.5 -91v-43h-171q-19 0 -31 -11.5t-12 -30.5q0 -20 12 -31.5t31 -11.5h43v-555q0 -54 36.5 -91t91.5 -37h426q55 0 91.5 37t36.5 91v555h43q19 0 31 11.5t12 31.5q0 19 -12 30.5t-31 11.5v0zM384 768
+q0 19 11.5 31t31.5 12h170q20 0 31.5 -12t11.5 -31v-43h-256v43v0zM768 85q0 -19 -11.5 -30.5t-31.5 -11.5h-426q-20 0 -31.5 11.5t-11.5 30.5v555h512v-555v0z" />
+    <glyph glyph-name="uniE9E2" unicode="&#xe9e2;" 
+d="M896 725h-171v43q0 54 -36.5 91t-91.5 37h-170q-55 0 -91.5 -37t-36.5 -91v-43h-171q-19 0 -31 -11.5t-12 -30.5q0 -20 12 -31.5t31 -11.5h43v-555q0 -54 36.5 -91t91.5 -37h426q55 0 91.5 37t36.5 91v555h43q19 0 31 11.5t12 31.5q0 19 -12 30.5t-31 11.5v0zM384 768
+q0 19 11.5 31t31.5 12h170q20 0 31.5 -12t11.5 -31v-43h-256v43v0zM768 85q0 -19 -11.5 -30.5t-31.5 -11.5h-426q-20 0 -31.5 11.5t-11.5 30.5v555h512v-555v0zM427 512q-20 0 -31.5 -11.5t-11.5 -31.5v-256q0 -19 11.5 -30.5t31.5 -11.5q19 0 30.5 11.5t11.5 30.5v256
+q0 20 -11.5 31.5t-30.5 11.5v0zM597 512q-19 0 -30.5 -11.5t-11.5 -31.5v-256q0 -19 11.5 -30.5t30.5 -11.5q20 0 31.5 11.5t11.5 30.5v256q0 20 -11.5 31.5t-31.5 11.5v0z" />
+    <glyph glyph-name="uniE9E3" unicode="&#xe9e3;" 
+d="M1024 427q0 19 -11.5 30.5t-31.5 11.5q-19 0 -30.5 -11.5t-11.5 -30.5v-154l-333 333q-13 13 -30 13t-30 -13l-183 -184l-290 291q-13 12 -30 12t-30 -12q-13 -13 -13 -30t13 -30l320 -320q13 -13 30 -13t30 13l183 183l303 -303h-154q-19 0 -30.5 -11.5t-11.5 -30.5
+q0 -20 11.5 -31.5t30.5 -11.5h256q4 0 9 0.5t8 3.5q7 3 13 9t9 13q3 3 3.5 8t0.5 9v256v0z" />
+    <glyph glyph-name="uniE9E4" unicode="&#xe9e4;" 
+d="M1020 700q-3 6 -9 12t-13 9q-3 3 -8 3.5t-9 0.5h-256q-19 0 -30.5 -11.5t-11.5 -30.5q0 -20 11.5 -31.5t30.5 -11.5h154l-303 -303l-183 184q-13 12 -30 12t-30 -12l-320 -320q-13 -13 -13 -30t13 -30q6 -7 13 -10t17 -3q9 0 16 3t14 10l290 290l183 -184q13 -12 30 -12
+t30 12l333 333v-153q0 -20 11.5 -31.5t30.5 -11.5q20 0 31.5 11.5t11.5 31.5v256q0 3 -0.5 8.5t-3.5 8.5v0z" />
+    <glyph glyph-name="uniE9E5" unicode="&#xe9e5;" 
+d="M981 192l-358 606q-13 22 -34.5 37.5t-46.5 22.5q-26 6 -51 3t-47 -16q-13 -7 -25 -18.5t-18 -24.5v0v0l-358 -610q-26 -45 -12 -97t59 -78q12 -10 27.5 -13.5t31.5 -3.5h726q25 0 49 9.5t40 28.5q19 20 29 42t10 48q-4 16 -8 33.5t-14 30.5v0zM905 98q-7 -6 -15.5 -9.5
+t-14.5 -3.5h-726q-6 0 -10.5 0.5t-10.5 4.5q-16 9 -19.5 26t2.5 33l363 602q3 3 6 8t6 5q16 9 33.5 4.5t26.5 -17.5l363 -602q3 -3 3.5 -9t0.5 -12q3 -10 -1 -16.5t-7 -13.5v0z" />
+    <glyph glyph-name="uniE9E6" unicode="&#xe9e6;" 
+d="M1011 499l-128 128q-6 7 -13 10t-17 3h-128v171q0 19 -11.5 30.5t-30.5 11.5h-640q-20 0 -31.5 -11.5t-11.5 -30.5v-555q0 -19 11.5 -31t31.5 -12h59q-9 -16 -13 -32t-4 -32q0 -60 44.5 -104.5t105.5 -44.5q60 0 104.5 44.5t44.5 104.5q0 16 -3.5 32t-13.5 32h286
+q-7 -16 -12 -32t-5 -32q0 -60 44 -104.5t105 -44.5t105 44.5t44 104.5q0 16 -3.5 32t-13.5 32h64q20 0 31.5 12t11.5 31v213q0 10 -3 17t-10 13v0zM85 768h555v-469h-555v469v0zM299 149q0 -25 -19.5 -44.5t-44.5 -19.5q-26 0 -45 19.5t-19 44.5q0 26 19 45t45 19
+q25 0 44.5 -19t19.5 -45zM853 149q0 -25 -19 -44.5t-45 -19.5q-25 0 -44.5 19.5t-19.5 44.5q0 26 19.5 45t44.5 19q26 0 45 -19t19 -45zM939 299h-214v256h111l103 -103v-153z" />
+    <glyph glyph-name="uniE9E7" unicode="&#xe9e7;" 
+d="M853 683h-239l141 140q13 13 13 30t-13 30t-30 13t-30 -13l-183 -183l-183 183q-13 13 -30 13t-30 -13t-13 -30t13 -30l141 -140h-239q-55 0 -91.5 -37t-36.5 -91v-470q0 -54 36.5 -91t91.5 -37h682q55 0 91.5 37t36.5 91v470q0 54 -36.5 91t-91.5 37v0zM896 85
+q0 -19 -11.5 -30.5t-31.5 -11.5h-682q-20 0 -31.5 11.5t-11.5 30.5v470q0 19 11.5 30.5t31.5 11.5h682q20 0 31.5 -11.5t11.5 -30.5v-470v0z" />
+    <glyph glyph-name="uniE9E8" unicode="&#xe9e8;" 
+d="M1007 845q-10 6 -23.5 6t-23.5 -6q-22 -16 -47.5 -29t-50.5 -22q-32 28 -72 43q-39 16 -80 16.5t-82 -13.5q-40 -13 -73 -42q-42 -35 -64 -81.5t-22 -97.5q-46 3 -90 16q-43 14 -82.5 36t-73.5 53t-61 70q-6 9 -15.5 13t-22.5 4t-22 -8t-12 -18q-4 -3 -25 -70.5
+t-18 -159.5q2 -40 12 -85q10 -44 33 -89t62 -88t98 -80q-48 -22 -101.5 -32t-107.5 -10q-13 0 -26.5 -8.5t-16.5 -21.5t3 -27t18 -20q40 -23 82 -39q41 -17 82.5 -28.5t83.5 -16.5q41 -6 81 -6q80 0 156 20t147 61q69 39 122 94t89.5 123.5t55.5 148.5t19 168v10.5v10.5
+q32 35 53.5 76t31.5 86q3 13 -2 24.5t-15 18.5v0zM862 683q-7 -7 -10 -18t-3 -21q3 -6 3.5 -12.5t0.5 -12.5q0 -77 -16 -146t-47.5 -128t-76.5 -106q-45 -48 -103 -81q-45 -26 -93 -42q-49 -16 -99.5 -22.5t-103.5 -2.5q-52 3 -105 16q42 12 80 29.5t74 42.5q12 7 16.5 16.5
+t4.5 22.5q0 12 -8 21.5t-18 12.5q-92 41 -141 98q-48 57 -67.5 116.5t-16.5 115.5q2 56 12 96q35 -35 77 -62q42 -28 89 -47t97 -28q51 -9 104 -8q16 0 29.5 13.5t13.5 29.5v43q0 32 12.5 62t38.5 53q45 41 108 36.5t101 -49.5q10 -10 21.5 -13t21.5 0q6 3 14.5 4.5
+t14.5 4.5q-6 -10 -12.5 -17.5t-12.5 -16.5v0z" />
+    <glyph glyph-name="uniE9E9" unicode="&#xe9e9;" 
+d="M853 811h-682q-20 0 -31.5 -12t-11.5 -31v-128q0 -19 11.5 -31t31.5 -12q19 0 30.5 12t11.5 31v85h256v-597h-85q-19 0 -31 -11.5t-12 -31.5q0 -19 12 -30.5t31 -11.5h256q19 0 31 11.5t12 30.5q0 20 -12 31.5t-31 11.5h-85v597h256v-85q0 -19 11.5 -31t30.5 -12
+q20 0 31.5 12t11.5 31v128q0 19 -11.5 31t-31.5 12v0z" />
+    <glyph glyph-name="uniE9EA" unicode="&#xe9ea;" 
+d="M465 892q-93 -8 -173 -47q-81 -39 -142.5 -100.5t-100.5 -141.5q-39 -81 -49 -172q0 -10 3 -19t10 -15q3 -7 11.5 -10t18.5 -3h426v-256q0 -70 50.5 -120.5t120.5 -50.5t120.5 50.5t50.5 120.5q0 19 -12 31t-31 12t-31 -12t-12 -31q0 -35 -25 -60t-60 -25t-60 25t-25 60
+v256h426q10 0 18.5 3t11.5 10q3 6 8 15t5 19q-10 106 -59 195q-48 89 -124 152t-173 94t-203 20v0zM512 469h-418q13 69 47 128q33 59 83 104t112 73q63 29 133 37q82 8 158 -15q76 -22 138 -68t106 -112q43 -67 59 -147h-418v0z" />
+    <glyph glyph-name="uniE9EB" unicode="&#xe9eb;" 
+d="M512 213q62 0 117 24q54 23 94.5 63.5t63.5 94.5q24 55 24 117v299q0 19 -12 30.5t-31 11.5t-31 -11.5t-12 -30.5v-299q0 -45 -16 -84q-17 -39 -45.5 -67.5t-67.5 -45.5q-39 -16 -84 -16t-84 16q-39 17 -67.5 45.5t-45.5 67.5q-16 39 -16 84v299q0 19 -12 30.5t-31 11.5
+t-31 -11.5t-12 -30.5v-299q0 -62 24 -117q23 -54 63.5 -94.5t94.5 -63.5q55 -24 117 -24v0zM853 85h-682q-20 0 -31.5 -11.5t-11.5 -30.5q0 -20 11.5 -31.5t31.5 -11.5h682q20 0 31.5 11.5t11.5 31.5q0 19 -11.5 30.5t-31.5 11.5z" />
+    <glyph glyph-name="uniE9EC" unicode="&#xe9ec;" 
+d="M811 512h-470v128q0 35 13 66t39 53q22 26 53 39t66 13v0v0q61 0 107.5 -38t58.5 -99q4 -16 20 -26.5t32 -7.5t26.5 17.5t7.5 33.5q-10 45 -34 83q-23 37 -57 64.5t-75 42.5t-86 15v0v0q-51 0 -97.5 -19t-81.5 -58q-39 -35 -58 -81.5t-19 -97.5v-128h-43q-54 0 -91 -37
+t-37 -91v-299q0 -54 37 -91t91 -37h598q54 0 91 37t37 91v299q0 54 -37 91t-91 37v0zM853 85q0 -19 -11.5 -30.5t-30.5 -11.5h-598q-19 0 -30.5 11.5t-11.5 30.5v299q0 19 11.5 31t30.5 12h598q19 0 30.5 -12t11.5 -31v-299v0z" />
+    <glyph glyph-name="uniE9ED" unicode="&#xe9ed;" 
+d="M896 341q-19 0 -31 -11.5t-12 -30.5v-171q0 -19 -11.5 -31t-30.5 -12h-598q-19 0 -30.5 12t-11.5 31v171q0 19 -12 30.5t-31 11.5t-31 -11.5t-12 -30.5v-171q0 -54 37 -91t91 -37h598q54 0 91 37t37 91v171q0 19 -12 30.5t-31 11.5v0zM329 567l140 141v-409
+q0 -20 12 -31.5t31 -11.5t31 11.5t12 31.5v409l140 -141q7 -6 14 -9t16 -3q10 0 17 3t13 9q13 13 13 30t-13 30l-213 214q-3 3 -6.5 5.5t-6.5 2.5q-6 3 -15 3t-19 -3q-3 -3 -6.5 -4t-6.5 -4l-213 -214q-13 -13 -13 -30t13 -30q13 -12 30 -12t30 12v0z" />
+    <glyph glyph-name="uniE9EE" unicode="&#xe9ee;" 
+d="M994 465q-35 61 -95.5 96.5t-130.5 35.5h-21q-24 71 -72 125q-47 54 -108.5 86.5t-133.5 42.5q-71 9 -143 -10q-77 -20 -137 -65.5t-102 -109.5q-41 -64 -51.5 -140.5t9.5 -150.5q12 -44 32.5 -83t52.5 -74q13 -13 30 -15t30 10q12 13 14.5 30t-10.5 30
+q-23 26 -40.5 57.5t-23.5 66.5q-16 57 -7 114.5t41 111.5q29 51 77.5 87.5t105.5 48.5q60 16 118 7q59 -9 108.5 -38t85.5 -76q37 -47 51 -106q3 -13 15 -21.5t28 -8.5h51q48 0 87.5 -24t61.5 -65q16 -29 19.5 -62.5t-6.5 -65.5q-9 -32 -30.5 -59.5t-50.5 -43.5
+q-16 -9 -21.5 -26t4.5 -33q6 -10 16 -16t22 -6q7 0 11 0.5t11 3.5q45 26 76 64.5t47 89.5q13 45 7.5 96t-28.5 96v0zM542 457q-3 3 -6.5 5.5t-6.5 2.5q-6 3 -15 3t-19 -3q-3 -3 -6.5 -4t-6.5 -4l-171 -171q-12 -13 -12 -30t12 -30q13 -13 30 -13t30 13l98 98v-281
+q0 -20 12 -31.5t31 -11.5t31 11.5t12 31.5v281l98 -98q6 -6 14.5 -9.5t15.5 -3.5q6 0 14.5 3.5t15.5 9.5q12 13 12 30t-12 30l-171 171v0z" />
+    <glyph glyph-name="uniE9EF" unicode="&#xe9ef;" 
+d="M683 341h-342q-44 0 -83 -16q-39 -17 -68 -45.5t-45 -67.5q-17 -39 -17 -84v-85q0 -20 11.5 -31.5t31.5 -11.5q19 0 30.5 11.5t11.5 31.5v85q0 54 37 91t91 37h342q54 0 91 -37t37 -91v-85q0 -20 11.5 -31.5t30.5 -11.5q20 0 31.5 11.5t11.5 31.5v85q0 45 -17 84
+q-16 39 -45 67.5t-68 45.5q-39 16 -83 16v0zM512 427q45 0 84 16q39 17 67.5 45.5t45.5 67.5q16 39 16 84t-16 84q-17 39 -45.5 67.5t-67.5 45.5q-39 16 -84 16t-84 -16q-39 -17 -67.5 -45.5t-45.5 -67.5q-16 -39 -16 -84t16 -84q17 -39 45.5 -67.5t67.5 -45.5
+q39 -16 84 -16zM512 768q54 0 91 -37t37 -91t-37 -91t-91 -37t-91 37t-37 91t37 91t91 37z" />
+    <glyph glyph-name="uniE9F0" unicode="&#xe9f0;" 
+d="M512 341h-299q-44 0 -83 -16q-39 -17 -68 -45.5t-45 -67.5q-17 -39 -17 -84v-85q0 -20 11.5 -31.5t31.5 -11.5q19 0 30.5 11.5t11.5 31.5v85q0 54 37 91t91 37h299q54 0 91 -37t37 -91v-85q0 -20 11.5 -31.5t31.5 -11.5q19 0 30.5 11.5t11.5 31.5v85q0 45 -16 84
+q-17 39 -45.5 67.5t-67.5 45.5q-39 16 -84 16v0zM363 427q44 0 83 16q39 17 68 45.5t45 67.5q17 39 17 84t-17 84q-16 39 -45 67.5t-68 45.5q-39 16 -83 16q-45 0 -84 -16q-39 -17 -68 -45.5t-45 -67.5q-17 -39 -17 -84t17 -84q16 -39 45 -67.5t68 -45.5q39 -16 84 -16v0z
+M363 768q54 0 91 -37t37 -91t-37 -91t-91 -37q-55 0 -91.5 37t-36.5 91t36.5 91t91.5 37zM1011 585q-13 12 -30 12t-30 -12l-140 -141l-56 55q-13 13 -30 13t-30 -13q-12 -13 -12 -30t12 -30l86 -85q6 -6 14.5 -9.5t15.5 -3.5q6 0 14.5 3.5t15.5 9.5l170 171q13 13 13 30
+t-13 30v0z" />
+    <glyph glyph-name="uniE9F1" unicode="&#xe9f1;" 
+d="M512 341h-299q-44 0 -83 -16q-39 -17 -68 -45.5t-45 -67.5q-17 -39 -17 -84v-85q0 -20 11.5 -31.5t31.5 -11.5q19 0 30.5 11.5t11.5 31.5v85q0 54 37 91t91 37h299q54 0 91 -37t37 -91v-85q0 -20 11.5 -31.5t31.5 -11.5q19 0 30.5 11.5t11.5 31.5v85q0 45 -16 84
+q-17 39 -45.5 67.5t-67.5 45.5q-39 16 -84 16v0zM363 427q44 0 83 16q39 17 68 45.5t45 67.5q17 39 17 84t-17 84q-16 39 -45 67.5t-68 45.5q-39 16 -83 16q-45 0 -84 -16q-39 -17 -68 -45.5t-45 -67.5q-17 -39 -17 -84t17 -84q16 -39 45 -67.5t68 -45.5q39 -16 84 -16v0z
+M363 768q54 0 91 -37t37 -91t-37 -91t-91 -37q-55 0 -91.5 37t-36.5 91t36.5 91t91.5 37zM981 512h-256q-19 0 -30.5 -11.5t-11.5 -31.5q0 -19 11.5 -30.5t30.5 -11.5h256q20 0 31.5 11.5t11.5 30.5q0 20 -11.5 31.5t-31.5 11.5v0z" />
+    <glyph glyph-name="uniE9F2" unicode="&#xe9f2;" 
+d="M512 341h-299q-44 0 -83 -16q-39 -17 -68 -45.5t-45 -67.5q-17 -39 -17 -84v-85q0 -20 11.5 -31.5t31.5 -11.5q19 0 30.5 11.5t11.5 31.5v85q0 54 37 91t91 37h299q54 0 91 -37t37 -91v-85q0 -20 11.5 -31.5t31.5 -11.5q19 0 30.5 11.5t11.5 31.5v85q0 45 -16 84
+q-17 39 -45.5 67.5t-67.5 45.5q-39 16 -84 16v0zM363 427q44 0 83 16q39 17 68 45.5t45 67.5q17 39 17 84t-17 84q-16 39 -45 67.5t-68 45.5q-39 16 -83 16q-45 0 -84 -16q-39 -17 -68 -45.5t-45 -67.5q-17 -39 -17 -84t17 -84q16 -39 45 -67.5t68 -45.5q39 -16 84 -16v0z
+M363 768q54 0 91 -37t37 -91t-37 -91t-91 -37q-55 0 -91.5 37t-36.5 91t36.5 91t91.5 37zM981 512h-85v85q0 20 -11.5 31.5t-31.5 11.5q-19 0 -30.5 -11.5t-11.5 -31.5v-85h-86q-19 0 -30.5 -11.5t-11.5 -31.5q0 -19 11.5 -30.5t30.5 -11.5h86v-86q0 -19 11.5 -30.5
+t30.5 -11.5q20 0 31.5 11.5t11.5 30.5v86h85q20 0 31.5 11.5t11.5 30.5q0 20 -11.5 31.5t-31.5 11.5v0z" />
+    <glyph glyph-name="uniE9F3" unicode="&#xe9f3;" 
+d="M512 341h-299q-44 0 -83 -16q-39 -17 -68 -45.5t-45 -67.5q-17 -39 -17 -84v-85q0 -20 11.5 -31.5t31.5 -11.5q19 0 30.5 11.5t11.5 31.5v85q0 54 37 91t91 37h299q54 0 91 -37t37 -91v-85q0 -20 11.5 -31.5t31.5 -11.5q19 0 30.5 11.5t11.5 31.5v85q0 45 -16 84
+q-17 39 -45.5 67.5t-67.5 45.5q-39 16 -84 16v0zM363 427q44 0 83 16q39 17 68 45.5t45 67.5q17 39 17 84t-17 84q-16 39 -45 67.5t-68 45.5q-39 16 -83 16q-45 0 -84 -16q-39 -17 -68 -45.5t-45 -67.5q-17 -39 -17 -84t17 -84q16 -39 45 -67.5t68 -45.5q39 -16 84 -16v0z
+M363 768q54 0 91 -37t37 -91t-37 -91t-91 -37q-55 0 -91.5 37t-36.5 91t36.5 91t91.5 37zM934 491l77 76q13 13 13 30t-13 30t-30 13t-30 -13l-76 -77l-77 77q-13 13 -30 13t-30 -13t-13 -30t13 -30l77 -76l-77 -77q-13 -13 -13 -30t13 -30q7 -6 15.5 -9.5t14.5 -3.5
+t14.5 3.5t15.5 9.5l77 77l76 -77q7 -6 15.5 -9.5t14.5 -3.5q7 0 15.5 3.5t14.5 9.5q13 13 13 30t-13 30l-77 77v0z" />
+    <glyph glyph-name="uniE9F4" unicode="&#xe9f4;" 
+d="M555 341h-342q-44 0 -83 -16q-39 -17 -68 -45.5t-45 -67.5q-17 -39 -17 -84v-85q0 -20 11.5 -31.5t31.5 -11.5q19 0 30.5 11.5t11.5 31.5v85q0 54 37 91t91 37h342q54 0 91 -37t37 -91v-85q0 -20 11.5 -31.5t30.5 -11.5q20 0 31.5 11.5t11.5 31.5v85q0 45 -17 84
+q-16 39 -45 67.5t-68 45.5q-39 16 -83 16v0zM384 427q45 0 84 16q39 17 67.5 45.5t45.5 67.5q16 39 16 84t-16 84q-17 39 -45.5 67.5t-67.5 45.5q-39 16 -84 16t-84 -16q-39 -17 -67.5 -45.5t-45.5 -67.5q-16 -39 -16 -84t16 -84q17 -39 45.5 -67.5t67.5 -45.5
+q39 -16 84 -16zM384 768q54 0 91 -37t37 -91t-37 -91t-91 -37t-91 37t-37 91t37 91t91 37zM862 333q-16 3 -32 -5.5t-19 -24.5q-4 -16 5 -32t25 -19q41 -10 67 -44.5t26 -79.5v-85q0 -20 12 -31.5t31 -11.5t31 11.5t12 31.5v85q3 74 -41 131.5t-117 73.5v0zM691 845
+q-16 6 -30.5 -2.5t-20.5 -27.5q-3 -16 5.5 -32t24.5 -19q51 -13 79 -58.5t15 -99.5q-10 -35 -34.5 -59.5t-59.5 -34.5q-16 -3 -26 -19t-4 -32q3 -16 15 -25t28 -9h5.5h2.5q58 16 99.5 56t54.5 97q11 45 5 88q-7 44 -27.5 80t-54.5 61q-34 26 -77 36v0z" />
+    <glyph glyph-name="uniE9F5" unicode="&#xe9f5;" 
+d="M1003 678q-10 7 -21.5 5t-21.5 -9l-235 -166v132q0 54 -36.5 91t-91.5 37h-469q-54 0 -91 -37t-37 -91v-427q0 -54 37 -91t91 -37h469q55 0 91.5 37t36.5 91v133l231 -167q6 -3 12.5 -5.5t12.5 -2.5q7 0 11 0.5t11 3.5q9 6 15 16t6 22v427q0 13 -6 22.5t-15 15.5v0z
+M640 213q0 -19 -11.5 -30.5t-31.5 -11.5h-469q-19 0 -31 11.5t-12 30.5v427q0 19 12 31t31 12h469q20 0 31.5 -12t11.5 -31v-427zM939 294l-184 133l184 132v-265v0z" />
+    <glyph glyph-name="uniE9F6" unicode="&#xe9f6;" 
+d="M1003 678q-10 7 -21.5 5t-21.5 -9l-230 -162l-5 4v124q0 54 -36.5 91t-91.5 37h-140q-20 0 -31.5 -11.5t-11.5 -31.5q0 -19 11.5 -30.5t31.5 -11.5h140q20 0 31.5 -12t11.5 -31v-141q0 -9 3 -16t10 -14l42 -42q10 -10 26.5 -12t29.5 7l188 133v-342q0 -19 11.5 -30.5
+t30.5 -11.5q20 0 31.5 11.5t11.5 30.5v427q0 13 -6 22.5t-15 15.5v0zM713 286v0v0l-470 469v0v0l-170 171q-13 13 -30 13t-30 -13t-13 -30t13 -30l98 -98q-48 -6 -79.5 -43t-31.5 -85v-427q0 -54 37 -91t91 -37h469q39 0 71.5 22.5t48.5 54.5l234 -235q7 -6 15.5 -9t14.5 -3
+q7 0 15.5 3t14.5 9q13 13 13 30t-13 30l-298 299v0zM640 213q0 -19 -11.5 -30.5t-31.5 -11.5h-469q-19 0 -31 11.5t-12 30.5v427q0 19 12 31t31 12h68l444 -444v-26v0z" />
+    <glyph glyph-name="uniE9F7" unicode="&#xe9f7;" 
+d="M789 683q-48 0 -90 -19q-43 -19 -75 -51t-51 -74q-18 -43 -18 -91q0 -42 15 -79.5t40 -69.5h-196q25 32 40 69.5t15 79.5q0 48 -18 91q-19 42 -51 74t-75 51q-42 19 -90 19t-91 -19t-75 -51t-50 -74q-19 -43 -19 -91t19 -91q18 -42 50 -74t75 -51t91 -19h554q48 0 91 19
+t75 51t50 74q19 43 19 91t-19 91q-18 42 -50 74t-75 51t-91 19zM85 448q0 61 44.5 105t105.5 44q60 0 104.5 -44t44.5 -105t-44.5 -105t-104.5 -44q-61 0 -105.5 44t-44.5 105v0zM789 299q-60 0 -104.5 44t-44.5 105t44.5 105t104.5 44q61 0 105.5 -44t44.5 -105t-44.5 -105
+t-105.5 -44z" />
+    <glyph glyph-name="uniE9F8" unicode="&#xe9f8;" 
+d="M486 764q-9 6 -23 4t-24 -9l-200 -162h-154q-19 0 -30.5 -11.5t-11.5 -30.5v-256q0 -20 11.5 -31.5t30.5 -11.5h154l200 -162q7 -3 13.5 -6t12.5 -3q3 0 8.5 0.5t8.5 4.5q10 6 18 15.5t8 22.5v597q3 13 -3 22.5t-19 16.5v0zM427 218l-145 115q-7 3 -13.5 5.5t-12.5 2.5
+h-128v171h128q6 0 12.5 2.5t13.5 6.5l145 115v-418v0z" />
+    <glyph glyph-name="uniE9F9" unicode="&#xe9f9;" 
+d="M486 764q-9 6 -23 4t-24 -9l-200 -162h-154q-19 0 -30.5 -11.5t-11.5 -30.5v-256q0 -20 11.5 -31.5t30.5 -11.5h154l200 -162q7 -3 13.5 -6t12.5 -3q3 0 8.5 0.5t8.5 4.5q10 6 18 15.5t8 22.5v597q3 13 -3 22.5t-19 16.5v0zM427 218l-145 115q-7 3 -13.5 5.5t-12.5 2.5
+h-128v171h128q6 0 12.5 2.5t13.5 6.5l145 115v-418v0zM691 606q-13 13 -30 13t-30 -13q-12 -13 -12 -30t12 -30q52 -51 52 -121.5t-52 -121.5q-12 -13 -12 -30t12 -30q7 -6 15.5 -9.5t14.5 -3.5q7 0 15.5 3.5t14.5 9.5q39 39 58 86q19 48 19 97t-19 96t-58 84v0z" />
+    <glyph glyph-name="uniE9FA" unicode="&#xe9fa;" 
+d="M486 764q-9 6 -23 4t-24 -9l-200 -162h-154q-19 0 -30.5 -11.5t-11.5 -30.5v-256q0 -20 11.5 -31.5t30.5 -11.5h154l200 -162q7 -3 13.5 -6t12.5 -3q3 0 8.5 0.5t8.5 4.5q10 6 18 15.5t8 22.5v597q3 13 -3 22.5t-19 16.5v0zM427 218l-145 115q-7 3 -13.5 5.5t-12.5 2.5
+h-128v171h128q6 0 12.5 2.5t13.5 6.5l145 115v-418v0zM631 606q-12 -13 -12 -30t12 -30q52 -51 52 -121.5t-52 -121.5q-12 -13 -12 -30t12 -30q7 -6 15.5 -9.5t14.5 -3.5q7 0 15.5 3.5t14.5 9.5q37 37 55 84q19 47 19 96t-19 97q-18 47 -55 86q-9 13 -28 13t-32 -13v0z
+M845 759q-13 13 -30 13t-30 -13q-13 -12 -13 -29t13 -30q56 -56 84 -127t28 -144.5t-28 -143.5q-28 -71 -84 -127q-13 -13 -13 -30t13 -30q6 -6 15 -9.5t15 -3.5t14.5 3.5t15.5 9.5q69 67 103 153t34 176t-34 177t-103 155v0z" />
+    <glyph glyph-name="uniE9FB" unicode="&#xe9fb;" 
+d="M486 764q-9 6 -23 4t-24 -9l-200 -162h-154q-19 0 -30.5 -11.5t-11.5 -30.5v-256q0 -20 11.5 -31.5t30.5 -11.5h154l200 -162q7 -3 13.5 -6t12.5 -3q3 0 8.5 0.5t8.5 4.5q10 6 18 15.5t8 22.5v597q3 13 -3 22.5t-19 16.5v0zM427 218l-145 115q-7 3 -13.5 5.5t-12.5 2.5
+h-128v171h128q6 0 12.5 2.5t13.5 6.5l145 115v-418v0zM913 427l98 98q13 13 13 30t-13 30q-13 12 -30 12t-30 -12l-98 -99l-98 99q-13 12 -30 12t-30 -12q-12 -13 -12 -30t12 -30l99 -98l-99 -98q-12 -13 -12 -30t12 -30q7 -7 15.5 -10t14.5 -3q7 0 15.5 3t14.5 10l98 98
+l98 -98q7 -7 15.5 -10t14.5 -3q7 0 15.5 3t14.5 10q13 13 13 30t-13 30l-98 98v0z" />
+    <glyph glyph-name="uniE9FC" unicode="&#xe9fc;" 
+d="M853 427q0 73 -29 136t-77 111l-13 149q-7 48 -42 82t-86 34v0v0h-188q-48 0 -84.5 -34t-43.5 -82l-13 -149q-48 -45 -77 -109.5t-29 -137.5q0 -74 29 -138.5t77 -109.5l13 -149q7 -48 43.5 -81.5t84.5 -33.5v0v0h184v0v0q51 0 86 33.5t42 81.5l12 145q52 48 81.5 113
+t29.5 139v0zM375 815q0 16 13.5 27t29.5 11h188q16 0 29.5 -11t13.5 -27l8 -77q-35 13 -71 21.5t-74 8.5t-73.5 -8.5t-67.5 -21.5zM256 427q0 52 20 99t55 81.5t82 54.5q46 21 99 21t99 -21q47 -20 82 -54.5t55 -81.5t20 -99q0 -53 -20 -100q-20 -46 -55 -81t-82 -55
+q-46 -20 -99 -20t-99 20q-47 20 -82 55t-55 81q-20 47 -20 100zM649 38q0 -16 -13.5 -27t-29.5 -11v0v0h-188v0v0q-16 0 -27.5 11t-15.5 27l-8 77q32 -16 67 -23t74 -7q38 0 73.5 8.5t67.5 21.5v-77zM546 333q7 -7 15.5 -10t14.5 -3t14.5 3t15.5 10q13 13 13 30t-13 30
+l-51 51v111q0 19 -12 30.5t-31 11.5t-31 -11.5t-12 -30.5v-128q0 -10 3.5 -17t9.5 -13l64 -64v0z" />
+    <glyph glyph-name="uniE9FD" unicode="&#xe9fd;" 
+d="M188 435q-13 -13 -15 -30t10 -30q7 -6 16 -11.5t19 -5.5q6 0 14 3t11 6q59 50 130 74q70 25 143 25t144 -25q70 -24 129 -74q13 -13 31.5 -11t28.5 15q13 13 11 31.5t-15 28.5q-72 61 -157 91q-86 31 -173.5 31t-171.5 -29q-85 -30 -155 -89v0zM990 589q-103 89 -227 134
+t-253 45t-253 -45q-125 -45 -227 -134q-13 -13 -15 -30t11 -30q9 -10 16.5 -13.5t17.5 -3.5q6 0 14.5 2.5t15.5 6.5q91 80 201 120q109 40 222.5 40t222.5 -40t198 -120q13 -13 30 -11t30 15q13 16 12.5 33.5t-16.5 30.5v0zM341 286q-16 -10 -18.5 -27t10.5 -33
+q9 -13 26.5 -17t33.5 9q54 38 123.5 38t123.5 -38q6 -4 12.5 -6.5t13.5 -2.5q9 0 18 4t16 13q9 16 6.5 33t-15.5 27q-41 27 -87 41q-45 13 -91 13t-89 -13q-44 -14 -83 -41v0zM482 115q-6 -6 -9.5 -13t-3.5 -17q0 -9 3.5 -16t9.5 -14q7 -6 13.5 -9t16.5 -3t16.5 3t13.5 9
+q6 7 9.5 15.5t3.5 14.5q0 7 -3.5 15.5t-9.5 14.5q-13 13 -30 13t-30 -13v0z" />
+    <glyph glyph-name="uniE9FE" unicode="&#xe9fe;" 
+d="M1011 -13l-721 721v0v0l-217 218q-13 13 -30 13t-30 -13t-13 -30t13 -30l175 -175q-42 -22 -80.5 -48t-73.5 -58q-13 -13 -15 -30t11 -30q6 -7 13 -10t17 -3q6 0 14.5 3t15.5 10q35 32 76 57.5t86 44.5l98 -98q-45 -16 -86.5 -38.5t-80.5 -51.5q-12 -12 -14.5 -29
+t10.5 -30q7 -7 16 -12t18 -5q7 0 15 2.5t11 5.5q38 32 84.5 54.5t94.5 35.5l120 -120q-52 4 -103.5 -9.5t-93.5 -45.5q-16 -10 -18.5 -27t10.5 -33q9 -13 26.5 -17t33.5 9q54 38 123.5 38t123.5 -38q6 -4 12.5 -6.5t13.5 -2.5h4h4l282 -282q6 -6 14.5 -9t15.5 -3q6 0 14.5 3
+t14.5 9q10 13 9.5 30t-13.5 30v0zM674 486q-6 -16 -0.5 -32.5t21.5 -22.5q23 -13 45.5 -27t44.5 -33q6 -6 12.5 -7t13.5 -1q9 0 18 3.5t16 13.5q13 13 10.5 31.5t-14.5 27.5q-26 20 -52 38t-55 31q-16 6 -33 0t-27 -22v0zM461 683q64 4 128 -3q64 -8 124.5 -28t116.5 -52
+t104 -75q7 -7 14 -8t16 -1q10 0 19 3.5t15 9.5q13 13 11 30t-15 30q-54 48 -117 84q-64 37 -132.5 60t-141.5 32t-146 3q-20 0 -31 -14t-8 -33q0 -19 12 -30.5t31 -7.5v0zM482 115q-6 -6 -9.5 -13t-3.5 -17q0 -9 3.5 -16t9.5 -14q7 -6 13.5 -9t16.5 -3t16.5 3t13.5 9
+q6 7 9.5 15.5t3.5 14.5q0 7 -3.5 15.5t-9.5 14.5q-13 13 -30 13t-30 -13v0z" />
+    <glyph glyph-name="uniE9FF" unicode="&#xe9ff;" 
+d="M597 299v0v0h-512q-19 0 -30.5 -12t-11.5 -31t11.5 -31t30.5 -12h512v0v0q10 0 17 -3t13 -9q7 -7 10 -14t3 -16q0 -10 -3 -17t-10 -13q-6 -7 -13 -10t-17 -3v0v0q-9 0 -16 3t-14 10q-12 13 -29 13t-30 -13t-13 -30t13 -30q19 -19 41.5 -28.5t47.5 -9.5v0v0q26 0 48.5 9.5
+t41.5 28.5t28.5 41.5t9.5 48.5q0 25 -9.5 49t-28.5 40q-16 19 -40 29t-50 10v0zM85 555h384v0v0q26 0 50 9.5t40 28.5q38 38 38 91t-38 88q-16 19 -40 29t-50 10q-25 0 -49 -10t-40 -29q-13 -13 -13 -30t13 -29q13 -13 30 -13t29 13q7 6 14 9t16 3v0v0q10 0 17 -3t13 -9
+q13 -13 13 -30t-13 -30q-6 -7 -13 -10t-17 -3v0v0h-384q-19 0 -30.5 -11.5t-11.5 -31.5q0 -19 11.5 -30.5t30.5 -11.5v0zM939 640q-45 45 -106.5 45t-102.5 -45q-13 -13 -13 -30t13 -30q12 -13 29.5 -13t29.5 13q20 19 45.5 19t44.5 -19t19 -44.5t-19 -44.5
+q-10 -10 -22 -13.5t-25 -3.5h-747q-19 0 -30.5 -12t-11.5 -31t11.5 -31t30.5 -12h747q29 0 56.5 12t50.5 31q41 42 41 103t-41 106v0z" />
+    <glyph glyph-name="uniEA00" unicode="&#xea00;" 
+d="M572 427l226 226q13 13 13 30t-13 30q-13 12 -30 12t-30 -12l-226 -227l-226 227q-13 12 -30 12t-30 -12q-13 -13 -13 -30t13 -30l226 -226l-226 -226q-13 -13 -13 -30t13 -30q7 -7 13.5 -10t16.5 -3t16.5 3t13.5 10l226 226l226 -226q7 -7 15.5 -10t14.5 -3t14.5 3
+t15.5 10q13 13 13 30t-13 30l-226 226v0z" />
+    <glyph glyph-name="uniEA01" unicode="&#xea01;" 
+d="M512 896q-98 0 -183 -37q-86 -36 -149.5 -100t-100.5 -149q-36 -86 -36 -183q0 -98 36 -183q37 -86 100.5 -149.5t149.5 -100.5q85 -37 183 -37t183 37q86 37 149.5 100.5t100.5 149.5q36 85 36 183q0 97 -36 183q-37 85 -100.5 149t-149.5 100q-85 37 -183 37zM512 43
+q-80 0 -150 30t-122 82t-82 122t-30 150t30 150t82 122t122 82t150 30t150 -30t122 -82t82 -122t30 -150t-30 -150t-82 -122t-122 -82t-150 -30zM670 585q-13 12 -30 12t-30 -12l-98 -99l-98 99q-13 12 -30 12t-30 -12q-13 -13 -13 -30t13 -30l98 -98l-98 -98
+q-13 -13 -13 -30t13 -30q7 -7 13.5 -10t16.5 -3t16.5 3t13.5 10l98 98l98 -98q7 -7 15.5 -10t14.5 -3t14.5 3t15.5 10q13 13 13 30t-13 30l-98 98l98 98q13 13 13 30t-13 30v0z" />
+    <glyph glyph-name="uniEA02" unicode="&#xea02;" 
+d="M811 853h-598q-54 0 -91 -36.5t-37 -91.5v-597q0 -54 37 -91t91 -37h598q54 0 91 37t37 91v597q0 55 -37 91.5t-91 36.5zM853 128q0 -19 -11.5 -31t-30.5 -12h-598q-19 0 -30.5 12t-11.5 31v597q0 20 11.5 31.5t30.5 11.5h598q19 0 30.5 -11.5t11.5 -31.5v-597zM670 585
+q-13 12 -30 12t-30 -12l-98 -99l-98 99q-13 12 -30 12t-30 -12q-13 -13 -13 -30t13 -30l98 -98l-98 -98q-13 -13 -13 -30t13 -30q7 -7 13.5 -10t16.5 -3t16.5 3t13.5 10l98 98l98 -98q7 -7 15.5 -10t14.5 -3t14.5 3t15.5 10q13 13 13 30t-13 30l-98 98l98 98q13 13 13 30
+t-13 30v0z" />
+    <glyph glyph-name="uniEA03" unicode="&#xea03;" 
+d="M934 529q-6 13 -15.5 19.5t-22.5 6.5h-337l38 294q4 13 -4 26.5t-21 16.5q-13 6 -27.5 3t-23.5 -16l-427 -512q-7 -10 -9 -22t5 -25q6 -10 15.5 -15.5t22.5 -5.5h337l-38 -295q-4 -13 4 -26t21 -16q3 -4 8.5 -4.5t8.5 -0.5q10 0 19 4t15 13l427 512q7 10 9 21.5t-5 21.5
+v0zM529 141l26 196q0 10 -3 19t-6 15q-6 7 -15 10t-19 3h-294l273 329l-22 -197q0 -9 3 -18t6 -16q6 -6 14.5 -9.5t15.5 -3.5h294l-273 -328v0z" />
+    <glyph glyph-name="uniEA04" unicode="&#xea04;" 
+d="M482 700l9 13l-9 -60q-3 -16 8 -30t31 -17h2h2q16 0 27.5 11t14.5 27l30 205q4 13 -4 26.5t-21 16.5q-13 6 -27.5 3t-23.5 -16l-103 -124q-13 -13 -10.5 -30t14.5 -30q16 -9 33.5 -8.5t26.5 13.5v0zM670 469h136l-47 -55q-12 -13 -10 -31.5t15 -28.5q6 -6 12.5 -7
+t12.5 -1q10 0 19 3.5t15 13.5l103 123q6 10 8.5 22.5t-4.5 24.5q-3 10 -12 16t-22 6h-226q-19 0 -31 -12t-12 -31t12 -31t31 -12v0zM1011 -13l-938 939q-13 13 -30 13t-30 -13t-13 -30t13 -30l273 -273l-188 -226q-6 -10 -8.5 -22t4.5 -25q3 -10 12 -15.5t22 -5.5h337
+l-38 -295q-4 -13 4 -26t21 -16q3 -4 8.5 -4.5t8.5 -0.5q10 0 19 4t15 13l184 222l264 -264q7 -7 15.5 -10t14.5 -3q7 0 15.5 3t14.5 10q13 9 13 25.5t-13 29.5v0zM218 384l123 149l150 -149h-273v0zM529 141l21 188l73 -73l-94 -115v0z" />
+    <glyph glyph-name="uniEA05" unicode="&#xea05;" 
+d="M926 73l-158 157q38 48 61.5 110t23.5 129q0 80 -30 150t-82 122t-122 82t-150 30t-150 -30t-122 -82t-82 -122t-30 -150t30 -150t82 -122t122 -82t150 -30q68 0 129.5 22t109.5 64l158 -158q7 -7 15.5 -10t14.5 -3t14.5 3t15.5 10q13 13 13 30t-13 30v0zM171 469
+q0 63 23 117q23 55 63.5 95t94.5 64q55 23 117 23q63 0 117 -23q55 -24 95 -64t64 -95q23 -54 23 -117q0 -60 -23.5 -115t-61.5 -94v0v0v0v0q-42 -41 -95.5 -63t-113.5 -22q-64 -2 -120 21q-55 22 -96 62t-64 95q-23 54 -23 116v0zM597 512h-85v85q0 20 -11.5 31.5
+t-31.5 11.5q-19 0 -30.5 -11.5t-11.5 -31.5v-85h-86q-19 0 -30.5 -11.5t-11.5 -31.5q0 -19 11.5 -30.5t30.5 -11.5h86v-86q0 -19 11.5 -30.5t30.5 -11.5q20 0 31.5 11.5t11.5 30.5v86h85q20 0 31.5 11.5t11.5 30.5q0 20 -11.5 31.5t-31.5 11.5v0z" />
+    <glyph glyph-name="uniEA06" unicode="&#xea06;" 
+d="M926 73l-158 157q38 48 61.5 110t23.5 129q0 80 -30 150t-82 122t-122 82t-150 30t-150 -30t-122 -82t-82 -122t-30 -150t30 -150t82 -122t122 -82t150 -30q68 0 129.5 22t109.5 64l158 -158q7 -7 15.5 -10t14.5 -3t14.5 3t15.5 10q13 13 13 30t-13 30v0zM171 469
+q0 63 23 117q23 55 63.5 95t94.5 64q55 23 117 23q63 0 117 -23q55 -24 95 -64t64 -95q23 -54 23 -117q0 -60 -23.5 -115t-61.5 -94v0v0v0v0q-42 -41 -95.5 -63t-113.5 -22q-64 -2 -120 21q-55 22 -96 62t-64 95q-23 54 -23 116v0zM597 512h-256q-19 0 -30.5 -11.5
+t-11.5 -31.5q0 -19 11.5 -30.5t30.5 -11.5h256q20 0 31.5 11.5t11.5 30.5q0 20 -11.5 31.5t-31.5 11.5v0z" />
+  </font>
+</defs></svg>

BIN
static/assets/fonts/feather/feather-webfont.ttf


BIN
static/assets/fonts/feather/feather-webfont.woff


File diff suppressed because it is too large
+ 0 - 0
static/assets/images/browsers/android-browser.svg


+ 1 - 0
static/assets/images/browsers/aol-explorer.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -32 100 100" preserveAspectRatio="xMinYMin"><g transform="translate(-208.45127,-644.63366)"><g transform="matrix(0.2576927,0,0,0.2576927,155.23992,508.16265)"><path d="M420,564.1C385.5,564.1 359.4,590.9 359.4,624.1C359.4,659.1 386.6,684.1 420,684.1C453.4,684.1 480.5,659.1 480.5,624.1C480.5,590.9 454.5,564.1 420,564.1z M420,595.8C434.9,595.7 447.1,608.4 447.1,624.1C447.1,639.7 434.9,652.4 420,652.4C405.1,652.4 392.9,639.7 392.9,624.1C392.9,608.4 405.1,595.8 420,595.8z" style="stroke:none;stroke-width:0.43820944"/><path d="M507,397.4C507,409 497.6,418.4 486,418.4C474.4,418.4 465,409 465,397.4C465,385.8 474.4,376.4 486,376.4C497.6,376.4 507,385.8 507,397.4z" style="stroke:none;stroke-width:0.1" transform="translate(85.630073,265.696)"/><path style="stroke:none;stroke-width:1px" d="M531.5,680.1L498.5,680.1L498.5,531.1L531.5,531.1L531.5,680.1z"/><path d="M208.5,680.1L268.5,531.1L299.5,531.1L358.5,680.1L316.5,680.1L309.5,659.1L257.5,659.1L250.5,680.1L208.5,680.1z M299.5,628.1L268.5,628.1L284,578.1L299.5,628.1z" style="fill-rule:evenodd;stroke:none;stroke-width:0.2"/></g></g></svg>

+ 1 - 0
static/assets/images/browsers/blackberry.svg

@@ -0,0 +1 @@
+<svg width="39" height="39" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin"><title>blackberry</title><desc>Created with Sketch.</desc><g><title>Layer 1</title><g fill-rule="evenodd" fill="none" id="Page-1"><path fill="#000" id="blackberry" d="m12.267,11.864c0,-1.264 -0.774,-2.864 -4.027,-2.864l-5.009,0l-1.424,6.588l5.222,0c4.077,0 5.238,-1.93 5.238,-3.724l0,0zm13.493,0c0,-1.264 -0.772,-2.864 -4.024,-2.864l-5.01,0l-1.423,6.587l5.219,0c4.079,0.001 5.238,-1.929 5.238,-3.723l0,0zm-15.3,9.915c0,-1.264 -0.774,-2.868 -4.027,-2.868l-5.009,0l-1.424,6.592l5.22,0c4.078,0 5.24,-1.935 5.24,-3.724zm13.493,0c0,-1.264 -0.775,-2.868 -4.025,-2.868l-5.009,0l-1.426,6.592l5.222,0c4.079,0 5.238,-1.935 5.238,-3.724l0,0zm14.117,-4.021c0,-1.265 -0.775,-2.868 -4.025,-2.868l-5.009,0l-1.426,6.591l5.22,0c4.079,0 5.24,-1.93 5.24,-3.723l0,0zm-1.946,10.323c0,-1.265 -0.773,-2.864 -4.025,-2.864l-5.009,0l-1.424,6.588l5.22,0c4.078,0 5.238,-1.935 5.238,-3.724zm-14.11,4.022c0,-1.27 -0.772,-2.873 -4.022,-2.873l-5.012,0l-1.424,6.591l5.22,0c4.079,0.001 5.238,-1.929 5.238,-3.718l0,0z"/></g></g></svg>

File diff suppressed because it is too large
+ 0 - 0
static/assets/images/browsers/camino.svg


Some files were not shown because too many files changed in this diff