...
 
Commits (53)
......@@ -62,6 +62,7 @@ config :pleroma, :instance,
upload_limit: 16_000_000,
registrations_open: true,
federating: true,
allow_relay: true,
rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,
public: true,
quarantined_instances: []
......@@ -73,10 +74,6 @@ config :pleroma, :fe,
redirect_root_no_login: "/main/all",
redirect_root_login: "/main/friends",
show_instance_panel: true,
show_who_to_follow_panel: false,
who_to_follow_provider:
"https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-osa-api.cgi?{{host}}+{{user}}",
who_to_follow_link: "https://vinayaka.distsn.org/?{{host}}+{{user}}",
scope_options_enabled: false,
collapse_message_with_subject: false
......
social.domain.tld {
tls user@domain.tld
log /var/log/caddy/pleroma_access.log
errors /var/log/caddy/pleroma_error.log
log /var/log/caddy/pleroma.log
gzip
proxy / localhost:4000 {
websocket
transparent
}
tls user@domain.tld {
# Remove the rest of the lines in here, if you want to support older devices
key_type p256
ciphers ECDHE-ECDSA-WITH-CHACHA20-POLY1305 ECDHE-RSA-WITH-CHACHA20-POLY1305 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-RSA-AES256-GCM-SHA384 ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-RSA-AES128-GCM-SHA256
}
header / {
X-XSS-Protection "1; mode=block"
X-Frame-Options "DENY"
X-Content-Type-Options "nosniff"
Referrer-Policy "same-origin"
Strict-Transport-Security "max-age=31536000; includeSubDomains;"
Expect-CT "enforce, max-age=2592000"
}
# If you do not want remote frontends to be able to access your Pleroma backend server, remove these lines.
# If you want to allow all origins access, remove the origin lines.
# To use this directive, you need the http.cors plugin for Caddy.
cors / {
origin https://halcyon.domain.tld
origin https://pinafore.domain.tld
......@@ -10,9 +34,13 @@ social.domain.tld {
allowed_headers Authorization,Content-Type,Idempotency-Key
exposed_headers Link,X-RateLimit-Reset,X-RateLimit-Limit,X-RateLimit-Remaining,X-Request-Id
}
# Stop removing lines here.
proxy / localhost:4000 {
websocket
transparent
# If you do not want to use the mediaproxy function, remove these lines.
# To use this directive, you need the http.cache plugin for Caddy.
cache {
match_path /proxy
default_max_age 720m
}
# Stop removing lines here.
}
defmodule Mix.Tasks.RelayFollow do
use Mix.Task
require Logger
alias Pleroma.Web.ActivityPub.Relay
@shortdoc "Follows a remote relay"
def run([target]) do
Mix.Task.run("app.start")
:ok = Relay.follow(target)
# put this task to sleep to allow the genserver to push out the messages
:timer.sleep(500)
end
end
defmodule Mix.Tasks.RelayUnfollow do
use Mix.Task
require Logger
alias Pleroma.Web.ActivityPub.Relay
@shortdoc "Follows a remote relay"
def run([target]) do
Mix.Task.run("app.start")
:ok = Relay.unfollow(target)
# put this task to sleep to allow the genserver to push out the messages
:timer.sleep(500)
end
end
......@@ -77,7 +77,7 @@ defmodule Pleroma.User do
changes =
%User{}
|> cast(params, [:bio, :name, :ap_id, :nickname, :info, :avatar])
|> validate_required([:name, :ap_id, :nickname])
|> validate_required([:name, :ap_id])
|> unique_constraint(:nickname)
|> validate_format(:nickname, @email_regex)
|> validate_length(:bio, max: 5000)
......@@ -457,13 +457,34 @@ defmodule Pleroma.User do
update_and_set_cache(cs)
end
def get_notified_from_activity_query(to) do
from(
u in User,
where: u.ap_id in ^to,
where: u.local == true
)
end
def get_notified_from_activity(%Activity{recipients: to, data: %{"type" => "Announce"} = data}) do
object = Object.normalize(data["object"])
actor = User.get_cached_by_ap_id(data["actor"])
# ensure that the actor who published the announced object appears only once
to =
if actor.nickname != nil do
to ++ [object.data["actor"]]
else
to
end
|> Enum.uniq()
query = get_notified_from_activity_query(to)
Repo.all(query)
end
def get_notified_from_activity(%Activity{recipients: to}) do
query =
from(
u in User,
where: u.ap_id in ^to,
where: u.local == true
)
query = get_notified_from_activity_query(to)
Repo.all(query)
end
......@@ -500,7 +521,8 @@ defmodule Pleroma.User do
u.nickname,
u.name
)
}
},
where: not is_nil(u.nickname)
)
q =
......@@ -579,7 +601,11 @@ defmodule Pleroma.User do
end
def local_user_query() do
from(u in User, where: u.local == true)
from(
u in User,
where: u.local == true,
where: not is_nil(u.nickname)
)
end
def deactivate(%User{} = user) do
......@@ -638,6 +664,25 @@ defmodule Pleroma.User do
end
end
def get_or_create_instance_user do
relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
if user = get_by_ap_id(relay_uri) do
user
else
changes =
%User{}
|> cast(%{}, [:ap_id, :nickname, :local])
|> put_change(:ap_id, relay_uri)
|> put_change(:nickname, nil)
|> put_change(:local, true)
|> put_change(:follower_address, relay_uri <> "/followers")
{:ok, user} = Repo.insert(changes)
user
end
end
# AP style
def public_key_from_info(%{
"source_data" => %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
......
......@@ -12,6 +12,24 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
@instance Application.get_env(:pleroma, :instance)
# For Announce activities, we filter the recipients based on following status for any actors
# that match actual users. See issue #164 for more information about why this is necessary.
def get_recipients(%{"type" => "Announce"} = data) do
recipients = (data["to"] || []) ++ (data["cc"] || [])
actor = User.get_cached_by_ap_id(data["actor"])
recipients
|> Enum.filter(fn recipient ->
case User.get_cached_by_ap_id(recipient) do
nil ->
true
user ->
User.following?(user, actor)
end
end)
end
def get_recipients(data) do
(data["to"] || []) ++ (data["cc"] || [])
end
......@@ -556,12 +574,23 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
"locked" => locked
},
avatar: avatar,
nickname: "#{data["preferredUsername"]}@#{URI.parse(data["id"]).host}",
name: data["name"],
follower_address: data["followers"],
bio: data["summary"]
}
# nickname can be nil because of virtual actors
user_data =
if data["preferredUsername"] do
Map.put(
user_data,
:nickname,
"#{data["preferredUsername"]}@#{URI.parse(data["id"]).host}"
)
else
Map.put(user_data, :nickname, nil)
end
{:ok, user_data}
end
......
......@@ -5,6 +5,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
alias Pleroma.{User, Object}
alias Pleroma.Web.ActivityPub.{ObjectView, UserView}
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.Federator
require Logger
......@@ -145,6 +146,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
json(conn, "ok")
end
def relay(conn, params) do
with %User{} = user <- Relay.get_actor(),
{:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(UserView.render("user.json", %{user: user}))
else
nil -> {:error, :not_found}
end
end
def errors(conn, {:error, :not_found}) do
conn
|> put_status(404)
......
defmodule Pleroma.Web.ActivityPub.Relay do
alias Pleroma.{User, Object, Activity}
alias Pleroma.Web.ActivityPub.ActivityPub
require Logger
def get_actor do
User.get_or_create_instance_user()
end
def follow(target_instance) do
with %User{} = local_user <- get_actor(),
%User{} = target_user <- User.get_or_fetch_by_ap_id(target_instance),
{:ok, activity} <- ActivityPub.follow(local_user, target_user) do
Logger.info("relay: followed instance: #{target_instance}; id=#{activity.data["id"]}")
else
e -> Logger.error("error: #{inspect(e)}")
end
:ok
end
def unfollow(target_instance) do
with %User{} = local_user <- get_actor(),
%User{} = target_user <- User.get_or_fetch_by_ap_id(target_instance),
{:ok, activity} <- ActivityPub.unfollow(local_user, target_user) do
Logger.info("relay: unfollowed instance: #{target_instance}: id=#{activity.data["id"]}")
else
e -> Logger.error("error: #{inspect(e)}")
end
:ok
end
def publish(%Activity{data: %{"type" => "Create"}} = activity) do
with %User{} = user <- get_actor(),
%Object{} = object <- Object.normalize(activity.data["object"]["id"]) do
ActivityPub.announce(user, object)
else
e -> Logger.error("error: #{inspect(e)}")
end
end
def publish(_), do: nil
end
......@@ -306,6 +306,24 @@ defmodule Pleroma.Web.ActivityPub.Utils do
@doc """
Make announce activity data for the given actor and object
"""
# for relayed messages, we only want to send to subscribers
def make_announce_data(
%User{ap_id: ap_id, nickname: nil} = user,
%Object{data: %{"id" => id}} = object,
activity_id
) do
data = %{
"type" => "Announce",
"actor" => ap_id,
"object" => id,
"to" => [user.follower_address],
"cc" => [],
"context" => object.data["context"]
}
if activity_id, do: Map.put(data, "id", activity_id), else: data
end
def make_announce_data(
%User{ap_id: ap_id} = user,
%Object{data: %{"id" => id}} = object,
......@@ -360,7 +378,12 @@ defmodule Pleroma.Web.ActivityPub.Utils do
if activity_id, do: Map.put(data, "id", activity_id), else: data
end
def add_announce_to_object(%Activity{data: %{"actor" => actor}}, object) do
def add_announce_to_object(
%Activity{
data: %{"actor" => actor, "cc" => ["https://www.w3.org/ns/activitystreams#Public"]}
},
object
) do
announcements =
if is_list(object.data["announcements"]), do: object.data["announcements"], else: []
......@@ -369,6 +392,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do
end
end
def add_announce_to_object(_, object), do: {:ok, object}
def remove_announce_from_object(%Activity{data: %{"actor" => actor}}, object) do
announcements =
if is_list(object.data["announcements"]), do: object.data["announcements"], else: []
......
......@@ -9,6 +9,35 @@ defmodule Pleroma.Web.ActivityPub.UserView do
alias Pleroma.Web.ActivityPub.Utils
import Ecto.Query
# the instance itself is not a Person, but instead an Application
def render("user.json", %{user: %{nickname: nil} = user}) do
{:ok, user} = WebFinger.ensure_keys_present(user)
{:ok, _, public_key} = Salmon.keys_from_pem(user.info["keys"])
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
public_key = :public_key.pem_encode([public_key])
%{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => user.ap_id,
"type" => "Application",
"following" => "#{user.ap_id}/following",
"followers" => "#{user.ap_id}/followers",
"inbox" => "#{user.ap_id}/inbox",
"name" => "Pleroma",
"summary" => "Virtual actor for Pleroma relay",
"url" => user.ap_id,
"manuallyApprovesFollowers" => false,
"publicKey" => %{
"id" => "#{user.ap_id}#main-key",
"owner" => user.ap_id,
"publicKeyPem" => public_key
},
"endpoints" => %{
"sharedInbox" => "#{Pleroma.Web.Endpoint.url()}/inbox"
}
}
end
def render("user.json", %{user: user}) do
{:ok, user} = WebFinger.ensure_keys_present(user)
{:ok, _, public_key} = Salmon.keys_from_pem(user.info["keys"])
......@@ -42,7 +71,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do
"image" => %{
"type" => "Image",
"url" => User.banner_url(user)
}
},
"tag" => user.info["source_data"]["tag"] || []
}
|> Map.merge(Utils.make_json_ld_header())
end
......
defmodule Pleroma.Web.CommonAPI do
alias Pleroma.{Repo, Activity, Object}
alias Pleroma.{User, Repo, Activity, Object}
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Formatter
......@@ -61,8 +61,13 @@ defmodule Pleroma.Web.CommonAPI do
do: visibility
def get_visibility(%{"in_reply_to_status_id" => status_id}) when not is_nil(status_id) do
inReplyTo = get_replied_to_activity(status_id)
Pleroma.Web.MastodonAPI.StatusView.get_visibility(inReplyTo.data["object"])
case get_replied_to_activity(status_id) do
nil ->
"public"
inReplyTo ->
Pleroma.Web.MastodonAPI.StatusView.get_visibility(inReplyTo.data["object"])
end
end
def get_visibility(_), do: "public"
......@@ -125,6 +130,18 @@ defmodule Pleroma.Web.CommonAPI do
end
def update(user) do
user =
with emoji <- emoji_from_profile(user),
source_data <- (user.info["source_data"] || %{}) |> Map.put("tag", emoji),
new_info <- Map.put(user.info, "source_data", source_data),
change <- User.info_changeset(user, %{info: new_info}),
{:ok, user} <- User.update_and_set_cache(change) do
user
else
_e ->
user
end
ActivityPub.update(%{
local: true,
to: [user.follower_address],
......
defmodule Pleroma.Web.CommonAPI.Utils do
alias Pleroma.{Repo, Object, Formatter, Activity}
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Endpoint
alias Pleroma.User
alias Calendar.Strftime
alias Comeonin.Pbkdf2
......@@ -195,4 +196,15 @@ defmodule Pleroma.Web.CommonAPI.Utils do
_ -> {:error, "Invalid password."}
end
end
def emoji_from_profile(%{info: info} = user) do
(Formatter.get_emoji(user.bio) ++ Formatter.get_emoji(user.name))
|> Enum.map(fn {shortcode, url} ->
%{
"type" => "Emoji",
"icon" => %{"type" => "Image", "url" => "#{Endpoint.url()}#{url}"},
"name" => ":#{shortcode}:"
}
end)
end
end
......@@ -4,6 +4,7 @@ defmodule Pleroma.Web.Federator do
alias Pleroma.Activity
alias Pleroma.Web.{WebFinger, Websub}
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
require Logger
......@@ -69,6 +70,11 @@ defmodule Pleroma.Web.Federator do
Logger.info(fn -> "Sending #{activity.data["id"]} out via Salmon" end)
Pleroma.Web.Salmon.publish(actor, activity)
if Mix.env() != :test do
Logger.info(fn -> "Relaying #{activity.data["id"]} out" end)
Pleroma.Web.ActivityPub.Relay.publish(activity)
end
end
Logger.info(fn -> "Sending #{activity.data["id"]} out via AP" end)
......
......@@ -184,7 +184,10 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do
retweeted_xml = to_simple_form(retweeted_activity, retweeted_user, true)
mentions = activity.recipients |> get_mentions
mentions =
([retweeted_user.ap_id] ++ activity.recipients)
|> Enum.uniq()
|> get_mentions()
[
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']},
......
......@@ -5,11 +5,23 @@ defmodule Pleroma.Web.Router do
@instance Application.get_env(:pleroma, :instance)
@federating Keyword.get(@instance, :federating)
@allow_relay Keyword.get(@instance, :allow_relay)
@public Keyword.get(@instance, :public)
@registrations_open Keyword.get(@instance, :registrations_open)
def user_fetcher(username) do
{:ok, Repo.get_by(User, %{nickname: username})}
def user_fetcher(username_or_email) do
{
:ok,
cond do
# First, try logging in as if it was a name
user = Repo.get_by(User, %{nickname: username_or_email}) ->
user
# If we get nil, we try using it as an email
user = Repo.get_by(User, %{email: username_or_email}) ->
user
end
}
end
pipeline :api do
......@@ -284,6 +296,10 @@ defmodule Pleroma.Web.Router do
get("/externalprofile/show", TwitterAPI.Controller, :external_profile)
end
pipeline :ap_relay do
plug(:accepts, ["activity+json"])
end
pipeline :ostatus do
plug(:accepts, ["xml", "atom", "html", "activity+json"])
end
......@@ -320,6 +336,13 @@ defmodule Pleroma.Web.Router do
end
if @federating do
if @allow_relay do
scope "/relay", Pleroma.Web.ActivityPub do
pipe_through(:ap_relay)
get("/", ActivityPubController, :relay)
end
end
scope "/", Pleroma.Web.ActivityPub do
pipe_through(:activitypub)
post("/users/:nickname/inbox", ActivityPubController, :inbox)
......
......@@ -172,10 +172,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
redirectRootLogin: Keyword.get(@instance_fe, :redirect_root_login),
chatDisabled: !Keyword.get(@instance_chat, :enabled),
showInstanceSpecificPanel: Keyword.get(@instance_fe, :show_instance_panel),
showWhoToFollowPanel: Keyword.get(@instance_fe, :show_who_to_follow_panel),
scopeOptionsEnabled: Keyword.get(@instance_fe, :scope_options_enabled),
whoToFollowProvider: Keyword.get(@instance_fe, :who_to_follow_provider),
whoToFollowLink: Keyword.get(@instance_fe, :who_to_follow_link),
collapseMessageWithSubject:
Keyword.get(@instance_fe, :collapse_message_with_subject)
}
......
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Pleroma</title><link rel=icon type=image/png href=/favicon.png><link rel=stylesheet href=/static/font/css/fontello.css><link rel=stylesheet href=/static/font/css/animation.css><link href=/static/css/app.e07fc4f73aee0cb0cbffee2eba536027.css rel=stylesheet></head><body style="display: none"><div id=app></div><script type=text/javascript src=/static/js/manifest.91829bef2424fbfc7631.js></script><script type=text/javascript src=/static/js/vendor.d590300dfdab65a9b597.js></script><script type=text/javascript src=/static/js/app.0a721b31076fdf8dbe6f.js></script></body></html>
\ No newline at end of file
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Pleroma</title><link rel=icon type=image/png href=/favicon.png><link rel=stylesheet href=/static/font/css/fontello.css><link rel=stylesheet href=/static/font/css/animation.css><link href=/static/css/app.b76dcba564bc8d13e27c546e95fbb72e.css rel=stylesheet></head><body style="display: none"><div id=app></div><script type=text/javascript src=/static/js/manifest.004e63f0a7e5719fbbe8.js></script><script type=text/javascript src=/static/js/vendor.48cf760f1485c83eb636.js></script><script type=text/javascript src=/static/js/app.d3eba781c5f30aabbb47.js></script></body></html>
\ No newline at end of file
Font license info
## Font Awesome
Copyright (C) 2016 by Dave Gandy
Author: Dave Gandy
License: SIL ()
Homepage: http://fortawesome.github.com/Font-Awesome/
## Entypo
Copyright (C) 2012 by Daniel Bruce
Author: Daniel Bruce
License: SIL (http://scripts.sil.org/OFL)
Homepage: http://www.entypo.com
## Fontelico
Copyright (C) 2012 by Fontello project
Author: Crowdsourced, for Fontello project
License: SIL (http://scripts.sil.org/OFL)
Homepage: http://fontello.com
This webfont is generated by http://fontello.com open source project.
================================================================================
Please, note, that you should obey original font licenses, used to make this
webfont pack. Details available in LICENSE.txt file.
- Usually, it's enough to publish content of LICENSE.txt file somewhere on your
site in "About" section.
- If your project is open-source, usually, it will be ok to make LICENSE.txt
file publicly available in your repository.
- Fonts, used in Fontello, don't require a clickable link on your site.
But any kind of additional authors crediting is welcome.
================================================================================
Comments on archive content
---------------------------
- /font/* - fonts in different formats
- /css/* - different kinds of css, for all situations. Should be ok with
twitter bootstrap. Also, you can skip <i> style and assign icon classes
directly to text elements, if you don't mind about IE7.
- demo.html - demo file, to show your webfont content
- LICENSE.txt - license info about source fonts, used to build your one.
- config.json - keeps your settings. You can import it back into fontello
anytime, to continue your work
Why so many CSS files ?
-----------------------
Because we like to fit all your needs :)
- basic file, <your_font_name>.css - is usually enough, it contains @font-face
and character code definitions
- *-ie7.css - if you need IE7 support, but still don't wish to put char codes
directly into html
- *-codes.css and *-ie7-codes.css - if you like to use your own @font-face
rules, but still wish to benefit from css generation. That can be very
convenient for automated asset build systems. When you need to update font -
no need to manually edit files, just override old version with archive
content. See fontello source code for examples.
- *-embedded.css - basic css file, but with embedded WOFF font, to avoid
CORS issues in Firefox and IE9+, when fonts are hosted on the separate domain.
We strongly recommend to resolve this issue by `Access-Control-Allow-Origin`
server headers. But if you ok with dirty hack - this file is for you. Note,
that data url moved to separate @font-face to avoid problems with <IE9, when
string is too long.
- animate.css - use it to get ideas about spinner rotation animation.
Attention for server setup
--------------------------
You MUST setup server to reply with proper `mime-types` for font files -
otherwise some browsers will fail to show fonts.
Usually, `apache` already has necessary settings, but `nginx` and other
webservers should be tuned. Here is list of mime types for our file extensions:
- `application/vnd.ms-fontobject` - eot
- `application/x-font-woff` - woff
- `application/x-font-ttf` - ttf
- `image/svg+xml` - svg
{
"name": "",
"css_prefix_text": "icon-",
"css_use_suffix": false,
"hinting": true,
"units_per_em": 1000,
"ascent": 850,
"glyphs": [
{
"uid": "9bd60140934a1eb9236fd7a8ab1ff6ba",
"css": "spin4",
"code": 59444,
"src": "fontelico"
},
{
"uid": "5211af474d3a9848f67f945e2ccaf143",
"css": "cancel",
"code": 59392,
"src": "fontawesome"
},
{
"uid": "eeec3208c90b7b48e804919d0d2d4a41",
"css": "upload",
"code": 59393,
"src": "fontawesome"
},
{
"uid": "2a6740fc2f9d0edea54205963f662594",
"css": "spin3",
"code": 59442,
"src": "fontelico"
},
{
"uid": "c6be5a58ee4e63a5ec399c2b0d15cf2c",
"css": "reply",
"code": 61714,
"src": "fontawesome"
},
{
"uid": "474656633f79ea2f1dad59ff63f6bf07",
"css": "star",
"code": 59394,
"src": "fontawesome"
},
{
"uid": "d17030afaecc1e1c22349b99f3c4992a",
"css": "star-empty",
"code": 59395,
"src": "fontawesome"
},
{
"uid": "09feb4465d9bd1364f4e301c9ddbaa92",
"css": "retweet",
"code": 59396,
"src": "fontawesome"
},
{
"uid": "7fd683b2c518ceb9e5fa6757f2276faa",
"css": "eye-off",
"code": 59397,
"src": "fontawesome"
},
{
"uid": "73ffeb70554099177620847206c12457",
"css": "binoculars",
"code": 61925,
"src": "fontawesome"
},
{
"uid": "9e1c33b6849ceb08db8acfaf02188b7d",
"css": "plus-squared",
"code": 59398,
"src": "entypo"
},
{
"uid": "e99461abfef3923546da8d745372c995",
"css": "cog",
"code": 59399,
"src": "fontawesome"
},
{
"uid": "1bafeeb1808a5fe24484c7890096901a",
"css": "user-plus",
"code": 62004,
"src": "fontawesome"
},
{
"uid": "559647a6f430b3aeadbecd67194451dd",
"css": "menu",
"code": 61641,
"src": "fontawesome"
},
{
"uid": "0d20938846444af8deb1920dc85a29fb",
"css": "logout",
"code": 59400,
"src": "fontawesome"
},
{
"uid": "ccddff8e8670dcd130e3cb55fdfc2fd0",
"css": "down-open",
"code": 59401,
"src": "fontawesome"
},
{
"uid": "44b9e75612c5fad5505edd70d071651f",
"css": "attach",
"code": 59402,
"src": "entypo"
},
{
"uid": "e15f0d620a7897e2035c18c80142f6d9",
"css": "link-ext",
"code": 61582,
"src": "fontawesome"
},
{
"uid": "381da2c2f7fd51f8de877c044d7f439d",
"css": "picture",
"code": 59403,
"src": "fontawesome"
},
{
"uid": "872d9516df93eb6b776cc4d94bd97dac",
"css": "video",
"code": 59404,
"src": "fontawesome"
},
{
"uid": "399ef63b1e23ab1b761dfbb5591fa4da",
"css": "right-open",
"code": 59405,
"src": "fontawesome"
},
{
"uid": "d870630ff8f81e6de3958ecaeac532f2",
"css": "left-open",
"code": 59406,
"src": "fontawesome"
},
{
"uid": "fe6697b391355dec12f3d86d6d490397",
"css": "up-open",
"code": 59407,
"src": "fontawesome"
},
{
"uid": "9c1376672bb4f1ed616fdd78a23667e9",
"css": "comment-empty",
"code": 61669,
"src": "fontawesome"
},
{
"uid": "cd21cbfb28ad4d903cede582157f65dc",
"css": "bell",
"code": 59408,
"src": "fontawesome"
}
]
}
\ No newline at end of file
/*
Animation example, for spinners
*/
.animate-spin {
-moz-animation: spin 2s infinite linear;
-o-animation: spin 2s infinite linear;
-webkit-animation: spin 2s infinite linear;
animation: spin 2s infinite linear;
display: inline-block;
}
@-moz-keyframes spin {
0% {
-moz-transform: rotate(0deg);
-o-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(359deg);
-o-transform: rotate(359deg);
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@-webkit-keyframes spin {
0% {
-moz-transform: rotate(0deg);
-o-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(359deg);
-o-transform: rotate(359deg);
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@-o-keyframes spin {
0% {
-moz-transform: rotate(0deg);
-o-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(359deg);
-o-transform: rotate(359deg);
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@-ms-keyframes spin {
0% {
-moz-transform: rotate(0deg);
-o-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(359deg);
-o-transform: rotate(359deg);
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@keyframes spin {
0% {
-moz-transform: rotate(0deg);
-o-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(359deg);
-o-transform: rotate(359deg);
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
.icon-cancel:before { content: '\e800'; } /* '' */
.icon-upload:before { content: '\e801'; } /* '' */
.icon-star:before { content: '\e802'; } /* '' */
.icon-star-empty:before { content: '\e803'; } /* '' */
.icon-retweet:before { content: '\e804'; } /* '' */
.icon-eye-off:before { content: '\e805'; } /* '' */
.icon-plus-squared:before { content: '\e806'; } /* '' */
.icon-cog:before { content: '\e807'; } /* '' */
.icon-logout:before { content: '\e808'; } /* '' */
.icon-down-open:before { content: '\e809'; } /* '' */
.icon-attach:before { content: '\e80a'; } /* '' */
.icon-picture:before { content: '\e80b'; } /* '' */
.icon-video:before { content: '\e80c'; } /* '' */
.icon-right-open:before { content: '\e80d'; } /* '' */
.icon-left-open:before { content: '\e80e'; } /* '' */
.icon-up-open:before { content: '\e80f'; } /* '' */
.icon-bell:before { content: '\e810'; } /* '' */
.icon-spin3:before { content: '\e832'; } /* '' */
.icon-spin4:before { content: '\e834'; } /* '' */
.icon-link-ext:before { content: '\f08e'; } /* '' */
.icon-menu:before { content: '\f0c9'; } /* '' */
.icon-comment-empty:before { content: '\f0e5'; } /* '' */
.icon-reply:before { content: '\f112'; } /* '' */
.icon-binoculars:before { content: '\f1e5'; } /* '' */
.icon-user-plus:before { content: '\f234'; } /* '' */
\ No newline at end of file
This diff is collapsed.
.icon-cancel { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe800;&nbsp;'); }
.icon-upload { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe801;&nbsp;'); }
.icon-star { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe802;&nbsp;'); }
.icon-star-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe803;&nbsp;'); }
.icon-retweet { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe804;&nbsp;'); }
.icon-eye-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe805;&nbsp;'); }
.icon-plus-squared { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe806;&nbsp;'); }
.icon-cog { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe807;&nbsp;'); }
.icon-logout { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe808;&nbsp;'); }
.icon-down-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe809;&nbsp;'); }
.icon-attach { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80a;&nbsp;'); }
.icon-picture { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80b;&nbsp;'); }
.icon-video { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80c;&nbsp;'); }
.icon-right-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80d;&nbsp;'); }
.icon-left-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80e;&nbsp;'); }
.icon-up-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80f;&nbsp;'); }
.icon-bell { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe810;&nbsp;'); }
.icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe832;&nbsp;'); }
.icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe834;&nbsp;'); }
.icon-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08e;&nbsp;'); }
.icon-menu { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0c9;&nbsp;'); }
.icon-comment-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0e5;&nbsp;'); }
.icon-reply { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf112;&nbsp;'); }
.icon-binoculars { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf1e5;&nbsp;'); }
.icon-user-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf234;&nbsp;'); }
\ No newline at end of file
[class^="icon-"], [class*=" icon-"] {
font-family: 'fontello';
font-style: normal;
font-weight: normal;
/* fix buttons height */
line-height: 1em;
/* you can be more comfortable with increased icons size */
/* font-size: 120%; */
}
.icon-cancel { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe800;&nbsp;'); }
.icon-upload { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe801;&nbsp;'); }
.icon-star { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe802;&nbsp;'); }
.icon-star-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe803;&nbsp;'); }
.icon-retweet { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe804;&nbsp;'); }
.icon-eye-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe805;&nbsp;'); }
.icon-plus-squared { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe806;&nbsp;'); }
.icon-cog { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe807;&nbsp;'); }
.icon-logout { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe808;&nbsp;'); }
.icon-down-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe809;&nbsp;'); }
.icon-attach { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80a;&nbsp;'); }
.icon-picture { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80b;&nbsp;'); }
.icon-video { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80c;&nbsp;'); }
.icon-right-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80d;&nbsp;'); }
.icon-left-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80e;&nbsp;'); }
.icon-up-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80f;&nbsp;'); }
.icon-bell { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe810;&nbsp;'); }
.icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe832;&nbsp;'); }
.icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe834;&nbsp;'); }
.icon-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08e;&nbsp;'); }
.icon-menu { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0c9;&nbsp;'); }
.icon-comment-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0e5;&nbsp;'); }
.icon-reply { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf112;&nbsp;'); }
.icon-binoculars { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf1e5;&nbsp;'); }
.icon-user-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf234;&nbsp;'); }
\ No newline at end of file
@font-face {
font-family: 'fontello';
src: url('../font/fontello.eot?47566415');
src: url('../font/fontello.eot?47566415#iefix') format('embedded-opentype'),
url('../font/fontello.woff2?47566415') format('woff2'),
url('../font/fontello.woff?47566415') format('woff'),
url('../font/fontello.ttf?47566415') format('truetype'),
url('../font/fontello.svg?47566415#fontello') format('svg');
font-weight: normal;
font-style: normal;
}
/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
/* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */
/*
@media screen and (-webkit-min-device-pixel-ratio:0) {
@font-face {
font-family: 'fontello';
src: url('../font/fontello.svg?47566415#fontello') format('svg');
}
}
*/
[class^="icon-"]:before, [class*=" icon-"]:before {
font-family: "fontello";
font-style: normal;
font-weight: normal;
speak: none;
display: inline-block;
text-decoration: inherit;
width: 1em;
margin-right: .2em;
text-align: center;
/* opacity: .8; */
/* For safety - reset parent styles, that can break glyph codes*/
font-variant: normal;
text-transform: none;
/* fix buttons height, for twitter bootstrap */
line-height: 1em;
/* Animation center compensation - margins should be symmetric */
/* remove if not needed */
margin-left: .2em;
/* you can be more comfortable with increased icons size */
/* font-size: 120%; */
/* Font smoothing. That was taken from TWBS */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* Uncomment for 3D effect */
/* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
}
.icon-cancel:before { content: '\e800'; } /* '' */
.icon-upload:before { content: '\e801'; } /* '' */
.icon-star:before { content: '\e802'; } /* '' */
.icon-star-empty:before { content: '\e803'; } /* '' */
.icon-retweet:before { content: '\e804'; } /* '' */
.icon-eye-off:before { content: '\e805'; } /* '' */
.icon-plus-squared:before { content: '\e806'; } /* '' */
.icon-cog:before { content: '\e807'; } /* '' */
.icon-logout:before { content: '\e808'; } /* '' */
.icon-down-open:before { content: '\e809'; } /* '' */
.icon-attach:before { content: '\e80a'; } /* '' */
.icon-picture:before { content: '\e80b'; } /* '' */
.icon-video:before { content: '\e80c'; } /* '' */
.icon-right-open:before { content: '\e80d'; } /* '' */
.icon-left-open:before { content: '\e80e'; } /* '' */
.icon-up-open:before { content: '\e80f'; } /* '' */
.icon-bell:before { content: '\e810'; } /* '' */
.icon-spin3:before { content: '\e832'; } /* '' */
.icon-spin4:before { content: '\e834'; } /* '' */
.icon-link-ext:before { content: '\f08e'; } /* '' */
.icon-menu:before { content: '\f0c9'; } /* '' */
.icon-comment-empty:before { content: '\f0e5'; } /* '' */
.icon-reply:before { content: '\f112'; } /* '' */
.icon-binoculars:before { content: '\f1e5'; } /* '' */
.icon-user-plus:before { content: '\f234'; } /* '' */
\ No newline at end of file
<!DOCTYPE html>
<html>
<head><!--[if lt IE 9]><script language="javascript" type="text/javascript" src="//html5shim.googlecode.com/svn/trunk/html5.js"></script><![endif]-->
<meta charset="UTF-8"><style>/*
* Bootstrap v2.2.1
*
* Copyright 2012 Twitter, Inc
* Licensed under the Apache License v2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
* Designed and built with all the love in the world @twitter by @mdo and @fat.
*/
.clearfix {
*zoom: 1;
}
.clearfix:before,
.clearfix:after {
display: table;
content: "";
line-height: 0;
}
.clearfix:after {
clear: both;
}
html {
font-size: 100%;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
a:focus {
outline: thin dotted #333;
outline: 5px auto -webkit-focus-ring-color;
outline-offset: -2px;
}
a:hover,
a:active {
outline: 0;
}
button,
input,
select,
textarea {
margin: 0;
font-size: 100%;
vertical-align: middle;
}
button,
input {
*overflow: visible;
line-height: normal;
}
button::-moz-focus-inner,
input::-moz-focus-inner {
padding: 0;
border: 0;
}
body {
margin: 0;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 20px;
color: #333;
background-color: #fff;
}
a {
color: #08c;
text-decoration: none;
}
a:hover {
color: #005580;
text-decoration: underline;
}
.row {
margin-left: -20px;
*zoom: 1;
}
.row:before,
.row:after {
display: table;
content: "";
line-height: 0;
}
.row:after {
clear: both;
}
[class*="span"] {
float: left;