diff options
Diffstat (limited to 'lib')
41 files changed, 2051 insertions, 190 deletions
| diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex index 11e4fde43..0e21408b2 100644 --- a/lib/mix/tasks/pleroma/config.ex +++ b/lib/mix/tasks/pleroma/config.ex @@ -45,7 +45,7 @@ defmodule Mix.Tasks.Pleroma.Config do      if Pleroma.Config.get([:instance, :dynamic_configuration]) do        config_path = "config/#{env}.exported_from_db.secret.exs" -      {:ok, file} = File.open(config_path, [:write]) +      {:ok, file} = File.open(config_path, [:write, :utf8])        IO.write(file, "use Mix.Config\r\n")        Repo.all(Config) diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index c1065611b..7e283df32 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -41,6 +41,10 @@ defmodule Pleroma.Activity do      field(:actor, :string)      field(:recipients, {:array, :string}, default: [])      field(:thread_muted?, :boolean, virtual: true) + +    # This is a fake relation, +    # do not use outside of with_preloaded_user_actor/with_joined_user_actor +    has_one(:user_actor, User, on_delete: :nothing, foreign_key: :id)      # This is a fake relation, do not use outside of with_preloaded_bookmark/get_bookmark      has_one(:bookmark, Bookmark)      has_many(:notifications, Notification, on_delete: :delete_all) @@ -86,6 +90,19 @@ defmodule Pleroma.Activity do      |> preload([activity, object: object], object: object)    end +  def with_joined_user_actor(query, join_type \\ :inner) do +    join(query, join_type, [activity], u in User, +      on: u.ap_id == activity.actor, +      as: :user_actor +    ) +  end + +  def with_preloaded_user_actor(query, join_type \\ :inner) do +    query +    |> with_joined_user_actor(join_type) +    |> preload([activity, user_actor: user_actor], user_actor: user_actor) +  end +    def with_preloaded_bookmark(query, %User{} = user) do      from([a] in query,        left_join: b in Bookmark, diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index d681eecc8..2b6a55f98 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -36,7 +36,8 @@ defmodule Pleroma.Application do          Pleroma.Emoji,          Pleroma.Captcha,          Pleroma.Daemons.ScheduledActivityDaemon, -        Pleroma.Daemons.ActivityExpirationDaemon +        Pleroma.Daemons.ActivityExpirationDaemon, +        Pleroma.Plugs.RateLimiter.Supervisor        ] ++          cachex_children() ++          hackney_pool_children() ++ diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex index 0bf20cdd0..1a432e681 100644 --- a/lib/pleroma/constants.ex +++ b/lib/pleroma/constants.ex @@ -9,6 +9,8 @@ defmodule Pleroma.Constants do    const(object_internal_fields,      do: [ +      "reactions", +      "reaction_count",        "likes",        "like_count",        "announcements", diff --git a/lib/pleroma/docs/json.ex b/lib/pleroma/docs/json.ex index 18ba01d58..f2a56d845 100644 --- a/lib/pleroma/docs/json.ex +++ b/lib/pleroma/docs/json.ex @@ -5,7 +5,7 @@ defmodule Pleroma.Docs.JSON do    def process(descriptions) do      config_path = "docs/generate_config.json" -    with {:ok, file} <- File.open(config_path, [:write]), +    with {:ok, file} <- File.open(config_path, [:write, :utf8]),           json <- generate_json(descriptions),           :ok <- IO.write(file, json),           :ok <- File.close(file) do diff --git a/lib/pleroma/emoji-data.txt b/lib/pleroma/emoji-data.txt new file mode 100644 index 000000000..2fb5c3ff6 --- /dev/null +++ b/lib/pleroma/emoji-data.txt @@ -0,0 +1,769 @@ +# emoji-data.txt +# Date: 2019-01-15, 12:10:05 GMT +# © 2019 Unicode®, Inc. +# Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries. +# For terms of use, see http://www.unicode.org/terms_of_use.html +# +# Emoji Data for UTS #51 +# Version: 12.0 +# +# For documentation and usage, see http://www.unicode.org/reports/tr51 +# +# Format:  +# <codepoint(s)> ; <property> # <comments>  +# Note: there is no guarantee as to the structure of whitespace or comments +# +# Characters and sequences are listed in code point order. Users should be shown a more natural order. +# See the CLDR collation order for Emoji. + + +# ================================================ + +# All omitted code points have Emoji=No  +# @missing: 0000..10FFFF  ; Emoji ; No + +0023          ; Emoji                #  1.1  [1] (#️)       number sign +002A          ; Emoji                #  1.1  [1] (*️)       asterisk +0030..0039    ; Emoji                #  1.1 [10] (0️..9️)    digit zero..digit nine +00A9          ; Emoji                #  1.1  [1] (©️)       copyright +00AE          ; Emoji                #  1.1  [1] (®️)       registered +203C          ; Emoji                #  1.1  [1] (‼️)       double exclamation mark +2049          ; Emoji                #  3.0  [1] (⁉️)       exclamation question mark +2122          ; Emoji                #  1.1  [1] (™️)       trade mark +2139          ; Emoji                #  3.0  [1] (ℹ️)       information +2194..2199    ; Emoji                #  1.1  [6] (↔️..↙️)    left-right arrow..down-left arrow +21A9..21AA    ; Emoji                #  1.1  [2] (↩️..↪️)    right arrow curving left..left arrow curving right +231A..231B    ; Emoji                #  1.1  [2] (⌚..⌛)    watch..hourglass done +2328          ; Emoji                #  1.1  [1] (⌨️)       keyboard +23CF          ; Emoji                #  4.0  [1] (⏏️)       eject button +23E9..23F3    ; Emoji                #  6.0 [11] (⏩..⏳)    fast-forward button..hourglass not done +23F8..23FA    ; Emoji                #  7.0  [3] (⏸️..⏺️)    pause button..record button +24C2          ; Emoji                #  1.1  [1] (Ⓜ️)       circled M +25AA..25AB    ; Emoji                #  1.1  [2] (▪️..▫️)    black small square..white small square +25B6          ; Emoji                #  1.1  [1] (▶️)       play button +25C0          ; Emoji                #  1.1  [1] (◀️)       reverse button +25FB..25FE    ; Emoji                #  3.2  [4] (◻️..◾)    white medium square..black medium-small square +2600..2604    ; Emoji                #  1.1  [5] (☀️..☄️)    sun..comet +260E          ; Emoji                #  1.1  [1] (☎️)       telephone +2611          ; Emoji                #  1.1  [1] (☑️)       check box with check +2614..2615    ; Emoji                #  4.0  [2] (☔..☕)    umbrella with rain drops..hot beverage +2618          ; Emoji                #  4.1  [1] (☘️)       shamrock +261D          ; Emoji                #  1.1  [1] (☝️)       index pointing up +2620          ; Emoji                #  1.1  [1] (☠️)       skull and crossbones +2622..2623    ; Emoji                #  1.1  [2] (☢️..☣️)    radioactive..biohazard +2626          ; Emoji                #  1.1  [1] (☦️)       orthodox cross +262A          ; Emoji                #  1.1  [1] (☪️)       star and crescent +262E..262F    ; Emoji                #  1.1  [2] (☮️..☯️)    peace symbol..yin yang +2638..263A    ; Emoji                #  1.1  [3] (☸️..☺️)    wheel of dharma..smiling face +2640          ; Emoji                #  1.1  [1] (♀️)       female sign +2642          ; Emoji                #  1.1  [1] (♂️)       male sign +2648..2653    ; Emoji                #  1.1 [12] (♈..♓)    Aries..Pisces +265F..2660    ; Emoji                #  1.1  [2] (♟️..♠️)    chess pawn..spade suit +2663          ; Emoji                #  1.1  [1] (♣️)       club suit +2665..2666    ; Emoji                #  1.1  [2] (♥️..♦️)    heart suit..diamond suit +2668          ; Emoji                #  1.1  [1] (♨️)       hot springs +267B          ; Emoji                #  3.2  [1] (♻️)       recycling symbol +267E..267F    ; Emoji                #  4.1  [2] (♾️..♿)    infinity..wheelchair symbol +2692..2697    ; Emoji                #  4.1  [6] (⚒️..⚗️)    hammer and pick..alembic +2699          ; Emoji                #  4.1  [1] (⚙️)       gear +269B..269C    ; Emoji                #  4.1  [2] (⚛️..⚜️)    atom symbol..fleur-de-lis +26A0..26A1    ; Emoji                #  4.0  [2] (⚠️..⚡)    warning..high voltage +26AA..26AB    ; Emoji                #  4.1  [2] (⚪..⚫)    white circle..black circle +26B0..26B1    ; Emoji                #  4.1  [2] (⚰️..⚱️)    coffin..funeral urn +26BD..26BE    ; Emoji                #  5.2  [2] (⚽..⚾)    soccer ball..baseball +26C4..26C5    ; Emoji                #  5.2  [2] (⛄..⛅)    snowman without snow..sun behind cloud +26C8          ; Emoji                #  5.2  [1] (⛈️)       cloud with lightning and rain +26CE          ; Emoji                #  6.0  [1] (⛎)       Ophiuchus +26CF          ; Emoji                #  5.2  [1] (⛏️)       pick +26D1          ; Emoji                #  5.2  [1] (⛑️)       rescue worker’s helmet +26D3..26D4    ; Emoji                #  5.2  [2] (⛓️..⛔)    chains..no entry +26E9..26EA    ; Emoji                #  5.2  [2] (⛩️..⛪)    shinto shrine..church +26F0..26F5    ; Emoji                #  5.2  [6] (⛰️..⛵)    mountain..sailboat +26F7..26FA    ; Emoji                #  5.2  [4] (⛷️..⛺)    skier..tent +26FD          ; Emoji                #  5.2  [1] (⛽)       fuel pump +2702          ; Emoji                #  1.1  [1] (✂️)       scissors +2705          ; Emoji                #  6.0  [1] (✅)       check mark button +2708..2709    ; Emoji                #  1.1  [2] (✈️..✉️)    airplane..envelope +270A..270B    ; Emoji                #  6.0  [2] (✊..✋)    raised fist..raised hand +270C..270D    ; Emoji                #  1.1  [2] (✌️..✍️)    victory hand..writing hand +270F          ; Emoji                #  1.1  [1] (✏️)       pencil +2712          ; Emoji                #  1.1  [1] (✒️)       black nib +2714          ; Emoji                #  1.1  [1] (✔️)       check mark +2716          ; Emoji                #  1.1  [1] (✖️)       multiplication sign +271D          ; Emoji                #  1.1  [1] (✝️)       latin cross +2721          ; Emoji                #  1.1  [1] (✡️)       star of David +2728          ; Emoji                #  6.0  [1] (✨)       sparkles +2733..2734    ; Emoji                #  1.1  [2] (✳️..✴️)    eight-spoked asterisk..eight-pointed star +2744          ; Emoji                #  1.1  [1] (❄️)       snowflake +2747          ; Emoji                #  1.1  [1] (❇️)       sparkle +274C          ; Emoji                #  6.0  [1] (❌)       cross mark +274E          ; Emoji                #  6.0  [1] (❎)       cross mark button +2753..2755    ; Emoji                #  6.0  [3] (❓..❕)    question mark..white exclamation mark +2757          ; Emoji                #  5.2  [1] (❗)       exclamation mark +2763..2764    ; Emoji                #  1.1  [2] (❣️..❤️)    heart exclamation..red heart +2795..2797    ; Emoji                #  6.0  [3] (➕..➗)    plus sign..division sign +27A1          ; Emoji                #  1.1  [1] (➡️)       right arrow +27B0          ; Emoji                #  6.0  [1] (➰)       curly loop +27BF          ; Emoji                #  6.0  [1] (➿)       double curly loop +2934..2935    ; Emoji                #  3.2  [2] (⤴️..⤵️)    right arrow curving up..right arrow curving down +2B05..2B07    ; Emoji                #  4.0  [3] (⬅️..⬇️)    left arrow..down arrow +2B1B..2B1C    ; Emoji                #  5.1  [2] (⬛..⬜)    black large square..white large square +2B50          ; Emoji                #  5.1  [1] (⭐)       star +2B55          ; Emoji                #  5.2  [1] (⭕)       hollow red circle +3030          ; Emoji                #  1.1  [1] (〰️)       wavy dash +303D          ; Emoji                #  3.2  [1] (〽️)       part alternation mark +3297          ; Emoji                #  1.1  [1] (㊗️)       Japanese “congratulations” button +3299          ; Emoji                #  1.1  [1] (㊙️)       Japanese “secret” button +1F004         ; Emoji                #  5.1  [1] (🀄)       mahjong red dragon +1F0CF         ; Emoji                #  6.0  [1] (🃏)       joker +1F170..1F171  ; Emoji                #  6.0  [2] (🅰️..🅱️)    A button (blood type)..B button (blood type) +1F17E         ; Emoji                #  6.0  [1] (🅾️)       O button (blood type) +1F17F         ; Emoji                #  5.2  [1] (🅿️)       P button +1F18E         ; Emoji                #  6.0  [1] (🆎)       AB button (blood type) +1F191..1F19A  ; Emoji                #  6.0 [10] (🆑..🆚)    CL button..VS button +1F1E6..1F1FF  ; Emoji                #  6.0 [26] (🇦..🇿)    regional indicator symbol letter a..regional indicator symbol letter z +1F201..1F202  ; Emoji                #  6.0  [2] (🈁..🈂️)    Japanese “here” button..Japanese “service charge” button +1F21A         ; Emoji                #  5.2  [1] (🈚)       Japanese “free of charge” button +1F22F         ; Emoji                #  5.2  [1] (🈯)       Japanese “reserved” button +1F232..1F23A  ; Emoji                #  6.0  [9] (🈲..🈺)    Japanese “prohibited” button..Japanese “open for business” button +1F250..1F251  ; Emoji                #  6.0  [2] (🉐..🉑)    Japanese “bargain” button..Japanese “acceptable” button +1F300..1F320  ; Emoji                #  6.0 [33] (🌀..🌠)    cyclone..shooting star +1F321         ; Emoji                #  7.0  [1] (🌡️)       thermometer +1F324..1F32C  ; Emoji                #  7.0  [9] (🌤️..🌬️)    sun behind small cloud..wind face +1F32D..1F32F  ; Emoji                #  8.0  [3] (🌭..🌯)    hot dog..burrito +1F330..1F335  ; Emoji                #  6.0  [6] (🌰..🌵)    chestnut..cactus +1F336         ; Emoji                #  7.0  [1] (🌶️)       hot pepper +1F337..1F37C  ; Emoji                #  6.0 [70] (🌷..🍼)    tulip..baby bottle +1F37D         ; Emoji                #  7.0  [1] (🍽️)       fork and knife with plate +1F37E..1F37F  ; Emoji                #  8.0  [2] (🍾..🍿)    bottle with popping cork..popcorn +1F380..1F393  ; Emoji                #  6.0 [20] (🎀..🎓)    ribbon..graduation cap +1F396..1F397  ; Emoji                #  7.0  [2] (🎖️..🎗️)    military medal..reminder ribbon +1F399..1F39B  ; Emoji                #  7.0  [3] (🎙️..🎛️)    studio microphone..control knobs +1F39E..1F39F  ; Emoji                #  7.0  [2] (🎞️..🎟️)    film frames..admission tickets +1F3A0..1F3C4  ; Emoji                #  6.0 [37] (🎠..🏄)    carousel horse..person surfing +1F3C5         ; Emoji                #  7.0  [1] (🏅)       sports medal +1F3C6..1F3CA  ; Emoji                #  6.0  [5] (🏆..🏊)    trophy..person swimming +1F3CB..1F3CE  ; Emoji                #  7.0  [4] (🏋️..🏎️)    person lifting weights..racing car +1F3CF..1F3D3  ; Emoji                #  8.0  [5] (🏏..🏓)    cricket game..ping pong +1F3D4..1F3DF  ; Emoji                #  7.0 [12] (🏔️..🏟️)    snow-capped mountain..stadium +1F3E0..1F3F0  ; Emoji                #  6.0 [17] (🏠..🏰)    house..castle +1F3F3..1F3F5  ; Emoji                #  7.0  [3] (🏳️..🏵️)    white flag..rosette +1F3F7         ; Emoji                #  7.0  [1] (🏷️)       label +1F3F8..1F3FF  ; Emoji                #  8.0  [8] (🏸..🏿)    badminton..dark skin tone +1F400..1F43E  ; Emoji                #  6.0 [63] (🐀..🐾)    rat..paw prints +1F43F         ; Emoji                #  7.0  [1] (🐿️)       chipmunk +1F440         ; Emoji                #  6.0  [1] (👀)       eyes +1F441         ; Emoji                #  7.0  [1] (👁️)       eye +1F442..1F4F7  ; Emoji                #  6.0[182] (👂..📷)    ear..camera +1F4F8         ; Emoji                #  7.0  [1] (📸)       camera with flash +1F4F9..1F4FC  ; Emoji                #  6.0  [4] (📹..📼)    video camera..videocassette +1F4FD         ; Emoji                #  7.0  [1] (📽️)       film projector +1F4FF         ; Emoji                #  8.0  [1] (📿)       prayer beads +1F500..1F53D  ; Emoji                #  6.0 [62] (🔀..🔽)    shuffle tracks button..downwards button +1F549..1F54A  ; Emoji                #  7.0  [2] (🕉️..🕊️)    om..dove +1F54B..1F54E  ; Emoji                #  8.0  [4] (🕋..🕎)    kaaba..menorah +1F550..1F567  ; Emoji                #  6.0 [24] (🕐..🕧)    one o’clock..twelve-thirty +1F56F..1F570  ; Emoji                #  7.0  [2] (🕯️..🕰️)    candle..mantelpiece clock +1F573..1F579  ; Emoji                #  7.0  [7] (🕳️..🕹️)    hole..joystick +1F57A         ; Emoji                #  9.0  [1] (🕺)       man dancing +1F587         ; Emoji                #  7.0  [1] (🖇️)       linked paperclips +1F58A..1F58D  ; Emoji                #  7.0  [4] (🖊️..🖍️)    pen..crayon +1F590         ; Emoji                #  7.0  [1] (🖐️)       hand with fingers splayed +1F595..1F596  ; Emoji                #  7.0  [2] (🖕..🖖)    middle finger..vulcan salute +1F5A4         ; Emoji                #  9.0  [1] (🖤)       black heart +1F5A5         ; Emoji                #  7.0  [1] (🖥️)       desktop computer +1F5A8         ; Emoji                #  7.0  [1] (🖨️)       printer +1F5B1..1F5B2  ; Emoji                #  7.0  [2] (🖱️..🖲️)    computer mouse..trackball +1F5BC         ; Emoji                #  7.0  [1] (🖼️)       framed picture +1F5C2..1F5C4  ; Emoji                #  7.0  [3] (🗂️..🗄️)    card index dividers..file cabinet +1F5D1..1F5D3  ; Emoji                #  7.0  [3] (🗑️..🗓️)    wastebasket..spiral calendar +1F5DC..1F5DE  ; Emoji                #  7.0  [3] (🗜️..🗞️)    clamp..rolled-up newspaper +1F5E1         ; Emoji                #  7.0  [1] (🗡️)       dagger +1F5E3         ; Emoji                #  7.0  [1] (🗣️)       speaking head +1F5E8         ; Emoji                #  7.0  [1] (🗨️)       left speech bubble +1F5EF         ; Emoji                #  7.0  [1] (🗯️)       right anger bubble +1F5F3         ; Emoji                #  7.0  [1] (🗳️)       ballot box with ballot +1F5FA         ; Emoji                #  7.0  [1] (🗺️)       world map +1F5FB..1F5FF  ; Emoji                #  6.0  [5] (🗻..🗿)    mount fuji..moai +1F600         ; Emoji                #  6.1  [1] (😀)       grinning face +1F601..1F610  ; Emoji                #  6.0 [16] (😁..😐)    beaming face with smiling eyes..neutral face +1F611         ; Emoji                #  6.1  [1] (😑)       expressionless face +1F612..1F614  ; Emoji                #  6.0  [3] (😒..😔)    unamused face..pensive face +1F615         ; Emoji                #  6.1  [1] (😕)       confused face +1F616         ; Emoji                #  6.0  [1] (😖)       confounded face +1F617         ; Emoji                #  6.1  [1] (😗)       kissing face +1F618         ; Emoji                #  6.0  [1] (😘)       face blowing a kiss +1F619         ; Emoji                #  6.1  [1] (😙)       kissing face with smiling eyes +1F61A         ; Emoji                #  6.0  [1] (😚)       kissing face with closed eyes +1F61B         ; Emoji                #  6.1  [1] (😛)       face with tongue +1F61C..1F61E  ; Emoji                #  6.0  [3] (😜..😞)    winking face with tongue..disappointed face +1F61F         ; Emoji                #  6.1  [1] (😟)       worried face +1F620..1F625  ; Emoji                #  6.0  [6] (😠..😥)    angry face..sad but relieved face +1F626..1F627  ; Emoji                #  6.1  [2] (😦..😧)    frowning face with open mouth..anguished face +1F628..1F62B  ; Emoji                #  6.0  [4] (😨..😫)    fearful face..tired face +1F62C         ; Emoji                #  6.1  [1] (😬)       grimacing face +1F62D         ; Emoji                #  6.0  [1] (😭)       loudly crying face +1F62E..1F62F  ; Emoji                #  6.1  [2] (😮..😯)    face with open mouth..hushed face +1F630..1F633  ; Emoji                #  6.0  [4] (😰..😳)    anxious face with sweat..flushed face +1F634         ; Emoji                #  6.1  [1] (😴)       sleeping face +1F635..1F640  ; Emoji                #  6.0 [12] (😵..🙀)    dizzy face..weary cat +1F641..1F642  ; Emoji                #  7.0  [2] (🙁..🙂)    slightly frowning face..slightly smiling face +1F643..1F644  ; Emoji                #  8.0  [2] (🙃..🙄)    upside-down face..face with rolling eyes +1F645..1F64F  ; Emoji                #  6.0 [11] (🙅..🙏)    person gesturing NO..folded hands +1F680..1F6C5  ; Emoji                #  6.0 [70] (🚀..🛅)    rocket..left luggage +1F6CB..1F6CF  ; Emoji                #  7.0  [5] (🛋️..🛏️)    couch and lamp..bed +1F6D0         ; Emoji                #  8.0  [1] (🛐)       place of worship +1F6D1..1F6D2  ; Emoji                #  9.0  [2] (🛑..🛒)    stop sign..shopping cart +1F6D5         ; Emoji                # 12.0  [1] (🛕)       hindu temple +1F6E0..1F6E5  ; Emoji                #  7.0  [6] (🛠️..🛥️)    hammer and wrench..motor boat +1F6E9         ; Emoji                #  7.0  [1] (🛩️)       small airplane +1F6EB..1F6EC  ; Emoji                #  7.0  [2] (🛫..🛬)    airplane departure..airplane arrival +1F6F0         ; Emoji                #  7.0  [1] (🛰️)       satellite +1F6F3         ; Emoji                #  7.0  [1] (🛳️)       passenger ship +1F6F4..1F6F6  ; Emoji                #  9.0  [3] (🛴..🛶)    kick scooter..canoe +1F6F7..1F6F8  ; Emoji                # 10.0  [2] (🛷..🛸)    sled..flying saucer +1F6F9         ; Emoji                # 11.0  [1] (🛹)       skateboard +1F6FA         ; Emoji                # 12.0  [1] (🛺)       auto rickshaw +1F7E0..1F7EB  ; Emoji                # 12.0 [12] (🟠..🟫)    orange circle..brown square +1F90D..1F90F  ; Emoji                # 12.0  [3] (🤍..🤏)    white heart..pinching hand +1F910..1F918  ; Emoji                #  8.0  [9] (🤐..🤘)    zipper-mouth face..sign of the horns +1F919..1F91E  ; Emoji                #  9.0  [6] (🤙..🤞)    call me hand..crossed fingers +1F91F         ; Emoji                # 10.0  [1] (🤟)       love-you gesture +1F920..1F927  ; Emoji                #  9.0  [8] (🤠..🤧)    cowboy hat face..sneezing face +1F928..1F92F  ; Emoji                # 10.0  [8] (🤨..🤯)    face with raised eyebrow..exploding head +1F930         ; Emoji                #  9.0  [1] (🤰)       pregnant woman +1F931..1F932  ; Emoji                # 10.0  [2] (🤱..🤲)    breast-feeding..palms up together +1F933..1F93A  ; Emoji                #  9.0  [8] (🤳..🤺)    selfie..person fencing +1F93C..1F93E  ; Emoji                #  9.0  [3] (🤼..🤾)    people wrestling..person playing handball +1F93F         ; Emoji                # 12.0  [1] (🤿)       diving mask +1F940..1F945  ; Emoji                #  9.0  [6] (🥀..🥅)    wilted flower..goal net +1F947..1F94B  ; Emoji                #  9.0  [5] (🥇..🥋)    1st place medal..martial arts uniform +1F94C         ; Emoji                # 10.0  [1] (🥌)       curling stone +1F94D..1F94F  ; Emoji                # 11.0  [3] (🥍..🥏)    lacrosse..flying disc +1F950..1F95E  ; Emoji                #  9.0 [15] (🥐..🥞)    croissant..pancakes +1F95F..1F96B  ; Emoji                # 10.0 [13] (🥟..🥫)    dumpling..canned food +1F96C..1F970  ; Emoji                # 11.0  [5] (🥬..🥰)    leafy green..smiling face with hearts +1F971         ; Emoji                # 12.0  [1] (🥱)       yawning face +1F973..1F976  ; Emoji                # 11.0  [4] (🥳..🥶)    partying face..cold face +1F97A         ; Emoji                # 11.0  [1] (🥺)       pleading face +1F97B         ; Emoji                # 12.0  [1] (🥻)       sari +1F97C..1F97F  ; Emoji                # 11.0  [4] (🥼..🥿)    lab coat..flat shoe +1F980..1F984  ; Emoji                #  8.0  [5] (🦀..🦄)    crab..unicorn +1F985..1F991  ; Emoji                #  9.0 [13] (🦅..🦑)    eagle..squid +1F992..1F997  ; Emoji                # 10.0  [6] (🦒..🦗)    giraffe..cricket +1F998..1F9A2  ; Emoji                # 11.0 [11] (🦘..🦢)    kangaroo..swan +1F9A5..1F9AA  ; Emoji                # 12.0  [6] (🦥..🦪)    sloth..oyster +1F9AE..1F9AF  ; Emoji                # 12.0  [2] (🦮..🦯)    guide dog..probing cane +1F9B0..1F9B9  ; Emoji                # 11.0 [10] (🦰..🦹)    red hair..supervillain +1F9BA..1F9BF  ; Emoji                # 12.0  [6] (🦺..🦿)    safety vest..mechanical leg +1F9C0         ; Emoji                #  8.0  [1] (🧀)       cheese wedge +1F9C1..1F9C2  ; Emoji                # 11.0  [2] (🧁..🧂)    cupcake..salt +1F9C3..1F9CA  ; Emoji                # 12.0  [8] (🧃..🧊)    beverage box..ice cube +1F9CD..1F9CF  ; Emoji                # 12.0  [3] (🧍..🧏)    person standing..deaf person +1F9D0..1F9E6  ; Emoji                # 10.0 [23] (🧐..🧦)    face with monocle..socks +1F9E7..1F9FF  ; Emoji                # 11.0 [25] (🧧..🧿)    red envelope..nazar amulet +1FA70..1FA73  ; Emoji                # 12.0  [4] (🩰..🩳)    ballet shoes..shorts +1FA78..1FA7A  ; Emoji                # 12.0  [3] (🩸..🩺)    drop of blood..stethoscope +1FA80..1FA82  ; Emoji                # 12.0  [3] (🪀..🪂)    yo-yo..parachute +1FA90..1FA95  ; Emoji                # 12.0  [6] (🪐..🪕)    ringed planet..banjo + +# Total elements: 1311 + +# ================================================ + +# All omitted code points have Emoji_Presentation=No  +# @missing: 0000..10FFFF  ; Emoji_Presentation ; No + +231A..231B    ; Emoji_Presentation   #  1.1  [2] (⌚..⌛)    watch..hourglass done +23E9..23EC    ; Emoji_Presentation   #  6.0  [4] (⏩..⏬)    fast-forward button..fast down button +23F0          ; Emoji_Presentation   #  6.0  [1] (⏰)       alarm clock +23F3          ; Emoji_Presentation   #  6.0  [1] (⏳)       hourglass not done +25FD..25FE    ; Emoji_Presentation   #  3.2  [2] (◽..◾)    white medium-small square..black medium-small square +2614..2615    ; Emoji_Presentation   #  4.0  [2] (☔..☕)    umbrella with rain drops..hot beverage +2648..2653    ; Emoji_Presentation   #  1.1 [12] (♈..♓)    Aries..Pisces +267F          ; Emoji_Presentation   #  4.1  [1] (♿)       wheelchair symbol +2693          ; Emoji_Presentation   #  4.1  [1] (⚓)       anchor +26A1          ; Emoji_Presentation   #  4.0  [1] (⚡)       high voltage +26AA..26AB    ; Emoji_Presentation   #  4.1  [2] (⚪..⚫)    white circle..black circle +26BD..26BE    ; Emoji_Presentation   #  5.2  [2] (⚽..⚾)    soccer ball..baseball +26C4..26C5    ; Emoji_Presentation   #  5.2  [2] (⛄..⛅)    snowman without snow..sun behind cloud +26CE          ; Emoji_Presentation   #  6.0  [1] (⛎)       Ophiuchus +26D4          ; Emoji_Presentation   #  5.2  [1] (⛔)       no entry +26EA          ; Emoji_Presentation   #  5.2  [1] (⛪)       church +26F2..26F3    ; Emoji_Presentation   #  5.2  [2] (⛲..⛳)    fountain..flag in hole +26F5          ; Emoji_Presentation   #  5.2  [1] (⛵)       sailboat +26FA          ; Emoji_Presentation   #  5.2  [1] (⛺)       tent +26FD          ; Emoji_Presentation   #  5.2  [1] (⛽)       fuel pump +2705          ; Emoji_Presentation   #  6.0  [1] (✅)       check mark button +270A..270B    ; Emoji_Presentation   #  6.0  [2] (✊..✋)    raised fist..raised hand +2728          ; Emoji_Presentation   #  6.0  [1] (✨)       sparkles +274C          ; Emoji_Presentation   #  6.0  [1] (❌)       cross mark +274E          ; Emoji_Presentation   #  6.0  [1] (❎)       cross mark button +2753..2755    ; Emoji_Presentation   #  6.0  [3] (❓..❕)    question mark..white exclamation mark +2757          ; Emoji_Presentation   #  5.2  [1] (❗)       exclamation mark +2795..2797    ; Emoji_Presentation   #  6.0  [3] (➕..➗)    plus sign..division sign +27B0          ; Emoji_Presentation   #  6.0  [1] (➰)       curly loop +27BF          ; Emoji_Presentation   #  6.0  [1] (➿)       double curly loop +2B1B..2B1C    ; Emoji_Presentation   #  5.1  [2] (⬛..⬜)    black large square..white large square +2B50          ; Emoji_Presentation   #  5.1  [1] (⭐)       star +2B55          ; Emoji_Presentation   #  5.2  [1] (⭕)       hollow red circle +1F004         ; Emoji_Presentation   #  5.1  [1] (🀄)       mahjong red dragon +1F0CF         ; Emoji_Presentation   #  6.0  [1] (🃏)       joker +1F18E         ; Emoji_Presentation   #  6.0  [1] (🆎)       AB button (blood type) +1F191..1F19A  ; Emoji_Presentation   #  6.0 [10] (🆑..🆚)    CL button..VS button +1F1E6..1F1FF  ; Emoji_Presentation   #  6.0 [26] (🇦..🇿)    regional indicator symbol letter a..regional indicator symbol letter z +1F201         ; Emoji_Presentation   #  6.0  [1] (🈁)       Japanese “here” button +1F21A         ; Emoji_Presentation   #  5.2  [1] (🈚)       Japanese “free of charge” button +1F22F         ; Emoji_Presentation   #  5.2  [1] (🈯)       Japanese “reserved” button +1F232..1F236  ; Emoji_Presentation   #  6.0  [5] (🈲..🈶)    Japanese “prohibited” button..Japanese “not free of charge” button +1F238..1F23A  ; Emoji_Presentation   #  6.0  [3] (🈸..🈺)    Japanese “application” button..Japanese “open for business” button +1F250..1F251  ; Emoji_Presentation   #  6.0  [2] (🉐..🉑)    Japanese “bargain” button..Japanese “acceptable” button +1F300..1F320  ; Emoji_Presentation   #  6.0 [33] (🌀..🌠)    cyclone..shooting star +1F32D..1F32F  ; Emoji_Presentation   #  8.0  [3] (🌭..🌯)    hot dog..burrito +1F330..1F335  ; Emoji_Presentation   #  6.0  [6] (🌰..🌵)    chestnut..cactus +1F337..1F37C  ; Emoji_Presentation   #  6.0 [70] (🌷..🍼)    tulip..baby bottle +1F37E..1F37F  ; Emoji_Presentation   #  8.0  [2] (🍾..🍿)    bottle with popping cork..popcorn +1F380..1F393  ; Emoji_Presentation   #  6.0 [20] (🎀..🎓)    ribbon..graduation cap +1F3A0..1F3C4  ; Emoji_Presentation   #  6.0 [37] (🎠..🏄)    carousel horse..person surfing +1F3C5         ; Emoji_Presentation   #  7.0  [1] (🏅)       sports medal +1F3C6..1F3CA  ; Emoji_Presentation   #  6.0  [5] (🏆..🏊)    trophy..person swimming +1F3CF..1F3D3  ; Emoji_Presentation   #  8.0  [5] (🏏..🏓)    cricket game..ping pong +1F3E0..1F3F0  ; Emoji_Presentation   #  6.0 [17] (🏠..🏰)    house..castle +1F3F4         ; Emoji_Presentation   #  7.0  [1] (🏴)       black flag +1F3F8..1F3FF  ; Emoji_Presentation   #  8.0  [8] (🏸..🏿)    badminton..dark skin tone +1F400..1F43E  ; Emoji_Presentation   #  6.0 [63] (🐀..🐾)    rat..paw prints +1F440         ; Emoji_Presentation   #  6.0  [1] (👀)       eyes +1F442..1F4F7  ; Emoji_Presentation   #  6.0[182] (👂..📷)    ear..camera +1F4F8         ; Emoji_Presentation   #  7.0  [1] (📸)       camera with flash +1F4F9..1F4FC  ; Emoji_Presentation   #  6.0  [4] (📹..📼)    video camera..videocassette +1F4FF         ; Emoji_Presentation   #  8.0  [1] (📿)       prayer beads +1F500..1F53D  ; Emoji_Presentation   #  6.0 [62] (🔀..🔽)    shuffle tracks button..downwards button +1F54B..1F54E  ; Emoji_Presentation   #  8.0  [4] (🕋..🕎)    kaaba..menorah +1F550..1F567  ; Emoji_Presentation   #  6.0 [24] (🕐..🕧)    one o’clock..twelve-thirty +1F57A         ; Emoji_Presentation   #  9.0  [1] (🕺)       man dancing +1F595..1F596  ; Emoji_Presentation   #  7.0  [2] (🖕..🖖)    middle finger..vulcan salute +1F5A4         ; Emoji_Presentation   #  9.0  [1] (🖤)       black heart +1F5FB..1F5FF  ; Emoji_Presentation   #  6.0  [5] (🗻..🗿)    mount fuji..moai +1F600         ; Emoji_Presentation   #  6.1  [1] (😀)       grinning face +1F601..1F610  ; Emoji_Presentation   #  6.0 [16] (😁..😐)    beaming face with smiling eyes..neutral face +1F611         ; Emoji_Presentation   #  6.1  [1] (😑)       expressionless face +1F612..1F614  ; Emoji_Presentation   #  6.0  [3] (😒..😔)    unamused face..pensive face +1F615         ; Emoji_Presentation   #  6.1  [1] (😕)       confused face +1F616         ; Emoji_Presentation   #  6.0  [1] (😖)       confounded face +1F617         ; Emoji_Presentation   #  6.1  [1] (😗)       kissing face +1F618         ; Emoji_Presentation   #  6.0  [1] (😘)       face blowing a kiss +1F619         ; Emoji_Presentation   #  6.1  [1] (😙)       kissing face with smiling eyes +1F61A         ; Emoji_Presentation   #  6.0  [1] (😚)       kissing face with closed eyes +1F61B         ; Emoji_Presentation   #  6.1  [1] (😛)       face with tongue +1F61C..1F61E  ; Emoji_Presentation   #  6.0  [3] (😜..😞)    winking face with tongue..disappointed face +1F61F         ; Emoji_Presentation   #  6.1  [1] (😟)       worried face +1F620..1F625  ; Emoji_Presentation   #  6.0  [6] (😠..😥)    angry face..sad but relieved face +1F626..1F627  ; Emoji_Presentation   #  6.1  [2] (😦..😧)    frowning face with open mouth..anguished face +1F628..1F62B  ; Emoji_Presentation   #  6.0  [4] (😨..😫)    fearful face..tired face +1F62C         ; Emoji_Presentation   #  6.1  [1] (😬)       grimacing face +1F62D         ; Emoji_Presentation   #  6.0  [1] (😭)       loudly crying face +1F62E..1F62F  ; Emoji_Presentation   #  6.1  [2] (😮..😯)    face with open mouth..hushed face +1F630..1F633  ; Emoji_Presentation   #  6.0  [4] (😰..😳)    anxious face with sweat..flushed face +1F634         ; Emoji_Presentation   #  6.1  [1] (😴)       sleeping face +1F635..1F640  ; Emoji_Presentation   #  6.0 [12] (😵..🙀)    dizzy face..weary cat +1F641..1F642  ; Emoji_Presentation   #  7.0  [2] (🙁..🙂)    slightly frowning face..slightly smiling face +1F643..1F644  ; Emoji_Presentation   #  8.0  [2] (🙃..🙄)    upside-down face..face with rolling eyes +1F645..1F64F  ; Emoji_Presentation   #  6.0 [11] (🙅..🙏)    person gesturing NO..folded hands +1F680..1F6C5  ; Emoji_Presentation   #  6.0 [70] (🚀..🛅)    rocket..left luggage +1F6CC         ; Emoji_Presentation   #  7.0  [1] (🛌)       person in bed +1F6D0         ; Emoji_Presentation   #  8.0  [1] (🛐)       place of worship +1F6D1..1F6D2  ; Emoji_Presentation   #  9.0  [2] (🛑..🛒)    stop sign..shopping cart +1F6D5         ; Emoji_Presentation   # 12.0  [1] (🛕)       hindu temple +1F6EB..1F6EC  ; Emoji_Presentation   #  7.0  [2] (🛫..🛬)    airplane departure..airplane arrival +1F6F4..1F6F6  ; Emoji_Presentation   #  9.0  [3] (🛴..🛶)    kick scooter..canoe +1F6F7..1F6F8  ; Emoji_Presentation   # 10.0  [2] (🛷..🛸)    sled..flying saucer +1F6F9         ; Emoji_Presentation   # 11.0  [1] (🛹)       skateboard +1F6FA         ; Emoji_Presentation   # 12.0  [1] (🛺)       auto rickshaw +1F7E0..1F7EB  ; Emoji_Presentation   # 12.0 [12] (🟠..🟫)    orange circle..brown square +1F90D..1F90F  ; Emoji_Presentation   # 12.0  [3] (🤍..🤏)    white heart..pinching hand +1F910..1F918  ; Emoji_Presentation   #  8.0  [9] (🤐..🤘)    zipper-mouth face..sign of the horns +1F919..1F91E  ; Emoji_Presentation   #  9.0  [6] (🤙..🤞)    call me hand..crossed fingers +1F91F         ; Emoji_Presentation   # 10.0  [1] (🤟)       love-you gesture +1F920..1F927  ; Emoji_Presentation   #  9.0  [8] (🤠..🤧)    cowboy hat face..sneezing face +1F928..1F92F  ; Emoji_Presentation   # 10.0  [8] (🤨..🤯)    face with raised eyebrow..exploding head +1F930         ; Emoji_Presentation   #  9.0  [1] (🤰)       pregnant woman +1F931..1F932  ; Emoji_Presentation   # 10.0  [2] (🤱..🤲)    breast-feeding..palms up together +1F933..1F93A  ; Emoji_Presentation   #  9.0  [8] (🤳..🤺)    selfie..person fencing +1F93C..1F93E  ; Emoji_Presentation   #  9.0  [3] (🤼..🤾)    people wrestling..person playing handball +1F93F         ; Emoji_Presentation   # 12.0  [1] (🤿)       diving mask +1F940..1F945  ; Emoji_Presentation   #  9.0  [6] (🥀..🥅)    wilted flower..goal net +1F947..1F94B  ; Emoji_Presentation   #  9.0  [5] (🥇..🥋)    1st place medal..martial arts uniform +1F94C         ; Emoji_Presentation   # 10.0  [1] (🥌)       curling stone +1F94D..1F94F  ; Emoji_Presentation   # 11.0  [3] (🥍..🥏)    lacrosse..flying disc +1F950..1F95E  ; Emoji_Presentation   #  9.0 [15] (🥐..🥞)    croissant..pancakes +1F95F..1F96B  ; Emoji_Presentation   # 10.0 [13] (🥟..🥫)    dumpling..canned food +1F96C..1F970  ; Emoji_Presentation   # 11.0  [5] (🥬..🥰)    leafy green..smiling face with hearts +1F971         ; Emoji_Presentation   # 12.0  [1] (🥱)       yawning face +1F973..1F976  ; Emoji_Presentation   # 11.0  [4] (🥳..🥶)    partying face..cold face +1F97A         ; Emoji_Presentation   # 11.0  [1] (🥺)       pleading face +1F97B         ; Emoji_Presentation   # 12.0  [1] (🥻)       sari +1F97C..1F97F  ; Emoji_Presentation   # 11.0  [4] (🥼..🥿)    lab coat..flat shoe +1F980..1F984  ; Emoji_Presentation   #  8.0  [5] (🦀..🦄)    crab..unicorn +1F985..1F991  ; Emoji_Presentation   #  9.0 [13] (🦅..🦑)    eagle..squid +1F992..1F997  ; Emoji_Presentation   # 10.0  [6] (🦒..🦗)    giraffe..cricket +1F998..1F9A2  ; Emoji_Presentation   # 11.0 [11] (🦘..🦢)    kangaroo..swan +1F9A5..1F9AA  ; Emoji_Presentation   # 12.0  [6] (🦥..🦪)    sloth..oyster +1F9AE..1F9AF  ; Emoji_Presentation   # 12.0  [2] (🦮..🦯)    guide dog..probing cane +1F9B0..1F9B9  ; Emoji_Presentation   # 11.0 [10] (🦰..🦹)    red hair..supervillain +1F9BA..1F9BF  ; Emoji_Presentation   # 12.0  [6] (🦺..🦿)    safety vest..mechanical leg +1F9C0         ; Emoji_Presentation   #  8.0  [1] (🧀)       cheese wedge +1F9C1..1F9C2  ; Emoji_Presentation   # 11.0  [2] (🧁..🧂)    cupcake..salt +1F9C3..1F9CA  ; Emoji_Presentation   # 12.0  [8] (🧃..🧊)    beverage box..ice cube +1F9CD..1F9CF  ; Emoji_Presentation   # 12.0  [3] (🧍..🧏)    person standing..deaf person +1F9D0..1F9E6  ; Emoji_Presentation   # 10.0 [23] (🧐..🧦)    face with monocle..socks +1F9E7..1F9FF  ; Emoji_Presentation   # 11.0 [25] (🧧..🧿)    red envelope..nazar amulet +1FA70..1FA73  ; Emoji_Presentation   # 12.0  [4] (🩰..🩳)    ballet shoes..shorts +1FA78..1FA7A  ; Emoji_Presentation   # 12.0  [3] (🩸..🩺)    drop of blood..stethoscope +1FA80..1FA82  ; Emoji_Presentation   # 12.0  [3] (🪀..🪂)    yo-yo..parachute +1FA90..1FA95  ; Emoji_Presentation   # 12.0  [6] (🪐..🪕)    ringed planet..banjo + +# Total elements: 1093 + +# ================================================ + +# All omitted code points have Emoji_Modifier=No  +# @missing: 0000..10FFFF  ; Emoji_Modifier ; No + +1F3FB..1F3FF  ; Emoji_Modifier       #  8.0  [5] (🏻..🏿)    light skin tone..dark skin tone + +# Total elements: 5 + +# ================================================ + +# All omitted code points have Emoji_Modifier_Base=No  +# @missing: 0000..10FFFF  ; Emoji_Modifier_Base ; No + +261D          ; Emoji_Modifier_Base  #  1.1  [1] (☝️)       index pointing up +26F9          ; Emoji_Modifier_Base  #  5.2  [1] (⛹️)       person bouncing ball +270A..270B    ; Emoji_Modifier_Base  #  6.0  [2] (✊..✋)    raised fist..raised hand +270C..270D    ; Emoji_Modifier_Base  #  1.1  [2] (✌️..✍️)    victory hand..writing hand +1F385         ; Emoji_Modifier_Base  #  6.0  [1] (🎅)       Santa Claus +1F3C2..1F3C4  ; Emoji_Modifier_Base  #  6.0  [3] (🏂..🏄)    snowboarder..person surfing +1F3C7         ; Emoji_Modifier_Base  #  6.0  [1] (🏇)       horse racing +1F3CA         ; Emoji_Modifier_Base  #  6.0  [1] (🏊)       person swimming +1F3CB..1F3CC  ; Emoji_Modifier_Base  #  7.0  [2] (🏋️..🏌️)    person lifting weights..person golfing +1F442..1F443  ; Emoji_Modifier_Base  #  6.0  [2] (👂..👃)    ear..nose +1F446..1F450  ; Emoji_Modifier_Base  #  6.0 [11] (👆..👐)    backhand index pointing up..open hands +1F466..1F478  ; Emoji_Modifier_Base  #  6.0 [19] (👦..👸)    boy..princess +1F47C         ; Emoji_Modifier_Base  #  6.0  [1] (👼)       baby angel +1F481..1F483  ; Emoji_Modifier_Base  #  6.0  [3] (💁..💃)    person tipping hand..woman dancing +1F485..1F487  ; Emoji_Modifier_Base  #  6.0  [3] (💅..💇)    nail polish..person getting haircut +1F48F         ; Emoji_Modifier_Base  #  6.0  [1] (💏)       kiss +1F491         ; Emoji_Modifier_Base  #  6.0  [1] (💑)       couple with heart +1F4AA         ; Emoji_Modifier_Base  #  6.0  [1] (💪)       flexed biceps +1F574..1F575  ; Emoji_Modifier_Base  #  7.0  [2] (🕴️..🕵️)    man in suit levitating..detective +1F57A         ; Emoji_Modifier_Base  #  9.0  [1] (🕺)       man dancing +1F590         ; Emoji_Modifier_Base  #  7.0  [1] (🖐️)       hand with fingers splayed +1F595..1F596  ; Emoji_Modifier_Base  #  7.0  [2] (🖕..🖖)    middle finger..vulcan salute +1F645..1F647  ; Emoji_Modifier_Base  #  6.0  [3] (🙅..🙇)    person gesturing NO..person bowing +1F64B..1F64F  ; Emoji_Modifier_Base  #  6.0  [5] (🙋..🙏)    person raising hand..folded hands +1F6A3         ; Emoji_Modifier_Base  #  6.0  [1] (🚣)       person rowing boat +1F6B4..1F6B6  ; Emoji_Modifier_Base  #  6.0  [3] (🚴..🚶)    person biking..person walking +1F6C0         ; Emoji_Modifier_Base  #  6.0  [1] (🛀)       person taking bath +1F6CC         ; Emoji_Modifier_Base  #  7.0  [1] (🛌)       person in bed +1F90F         ; Emoji_Modifier_Base  # 12.0  [1] (🤏)       pinching hand +1F918         ; Emoji_Modifier_Base  #  8.0  [1] (🤘)       sign of the horns +1F919..1F91E  ; Emoji_Modifier_Base  #  9.0  [6] (🤙..🤞)    call me hand..crossed fingers +1F91F         ; Emoji_Modifier_Base  # 10.0  [1] (🤟)       love-you gesture +1F926         ; Emoji_Modifier_Base  #  9.0  [1] (🤦)       person facepalming +1F930         ; Emoji_Modifier_Base  #  9.0  [1] (🤰)       pregnant woman +1F931..1F932  ; Emoji_Modifier_Base  # 10.0  [2] (🤱..🤲)    breast-feeding..palms up together +1F933..1F939  ; Emoji_Modifier_Base  #  9.0  [7] (🤳..🤹)    selfie..person juggling +1F93C..1F93E  ; Emoji_Modifier_Base  #  9.0  [3] (🤼..🤾)    people wrestling..person playing handball +1F9B5..1F9B6  ; Emoji_Modifier_Base  # 11.0  [2] (🦵..🦶)    leg..foot +1F9B8..1F9B9  ; Emoji_Modifier_Base  # 11.0  [2] (🦸..🦹)    superhero..supervillain +1F9BB         ; Emoji_Modifier_Base  # 12.0  [1] (🦻)       ear with hearing aid +1F9CD..1F9CF  ; Emoji_Modifier_Base  # 12.0  [3] (🧍..🧏)    person standing..deaf person +1F9D1..1F9DD  ; Emoji_Modifier_Base  # 10.0 [13] (🧑..🧝)    person..elf + +# Total elements: 120 + +# ================================================ + +# All omitted code points have Emoji_Component=No  +# @missing: 0000..10FFFF  ; Emoji_Component ; No + +0023          ; Emoji_Component      #  1.1  [1] (#️)       number sign +002A          ; Emoji_Component      #  1.1  [1] (*️)       asterisk +0030..0039    ; Emoji_Component      #  1.1 [10] (0️..9️)    digit zero..digit nine +200D          ; Emoji_Component      #  1.1  [1] ()        zero width joiner +20E3          ; Emoji_Component      #  3.0  [1] (⃣)       combining enclosing keycap +FE0F          ; Emoji_Component      #  3.2  [1] ()        VARIATION SELECTOR-16 +1F1E6..1F1FF  ; Emoji_Component      #  6.0 [26] (🇦..🇿)    regional indicator symbol letter a..regional indicator symbol letter z +1F3FB..1F3FF  ; Emoji_Component      #  8.0  [5] (🏻..🏿)    light skin tone..dark skin tone +1F9B0..1F9B3  ; Emoji_Component      # 11.0  [4] (🦰..🦳)    red hair..white hair +E0020..E007F  ; Emoji_Component      #  3.1 [96] (..)      tag space..cancel tag + +# Total elements: 146 + +# ================================================ + +# All omitted code points have Extended_Pictographic=No  +# @missing: 0000..10FFFF  ; Extended_Pictographic ; No + +00A9          ; Extended_Pictographic#  1.1  [1] (©️)       copyright +00AE          ; Extended_Pictographic#  1.1  [1] (®️)       registered +203C          ; Extended_Pictographic#  1.1  [1] (‼️)       double exclamation mark +2049          ; Extended_Pictographic#  3.0  [1] (⁉️)       exclamation question mark +2122          ; Extended_Pictographic#  1.1  [1] (™️)       trade mark +2139          ; Extended_Pictographic#  3.0  [1] (ℹ️)       information +2194..2199    ; Extended_Pictographic#  1.1  [6] (↔️..↙️)    left-right arrow..down-left arrow +21A9..21AA    ; Extended_Pictographic#  1.1  [2] (↩️..↪️)    right arrow curving left..left arrow curving right +231A..231B    ; Extended_Pictographic#  1.1  [2] (⌚..⌛)    watch..hourglass done +2328          ; Extended_Pictographic#  1.1  [1] (⌨️)       keyboard +2388          ; Extended_Pictographic#  3.0  [1] (⎈)       HELM SYMBOL +23CF          ; Extended_Pictographic#  4.0  [1] (⏏️)       eject button +23E9..23F3    ; Extended_Pictographic#  6.0 [11] (⏩..⏳)    fast-forward button..hourglass not done +23F8..23FA    ; Extended_Pictographic#  7.0  [3] (⏸️..⏺️)    pause button..record button +24C2          ; Extended_Pictographic#  1.1  [1] (Ⓜ️)       circled M +25AA..25AB    ; Extended_Pictographic#  1.1  [2] (▪️..▫️)    black small square..white small square +25B6          ; Extended_Pictographic#  1.1  [1] (▶️)       play button +25C0          ; Extended_Pictographic#  1.1  [1] (◀️)       reverse button +25FB..25FE    ; Extended_Pictographic#  3.2  [4] (◻️..◾)    white medium square..black medium-small square +2600..2605    ; Extended_Pictographic#  1.1  [6] (☀️..★)    sun..BLACK STAR +2607..2612    ; Extended_Pictographic#  1.1 [12] (☇..☒)    LIGHTNING..BALLOT BOX WITH X +2614..2615    ; Extended_Pictographic#  4.0  [2] (☔..☕)    umbrella with rain drops..hot beverage +2616..2617    ; Extended_Pictographic#  3.2  [2] (☖..☗)    WHITE SHOGI PIECE..BLACK SHOGI PIECE +2618          ; Extended_Pictographic#  4.1  [1] (☘️)       shamrock +2619          ; Extended_Pictographic#  3.0  [1] (☙)       REVERSED ROTATED FLORAL HEART BULLET +261A..266F    ; Extended_Pictographic#  1.1 [86] (☚..♯)    BLACK LEFT POINTING INDEX..MUSIC SHARP SIGN +2670..2671    ; Extended_Pictographic#  3.0  [2] (♰..♱)    WEST SYRIAC CROSS..EAST SYRIAC CROSS +2672..267D    ; Extended_Pictographic#  3.2 [12] (♲..♽)    UNIVERSAL RECYCLING SYMBOL..PARTIALLY-RECYCLED PAPER SYMBOL +267E..267F    ; Extended_Pictographic#  4.1  [2] (♾️..♿)    infinity..wheelchair symbol +2680..2685    ; Extended_Pictographic#  3.2  [6] (⚀..⚅)    DIE FACE-1..DIE FACE-6 +2690..2691    ; Extended_Pictographic#  4.0  [2] (⚐..⚑)    WHITE FLAG..BLACK FLAG +2692..269C    ; Extended_Pictographic#  4.1 [11] (⚒️..⚜️)    hammer and pick..fleur-de-lis +269D          ; Extended_Pictographic#  5.1  [1] (⚝)       OUTLINED WHITE STAR +269E..269F    ; Extended_Pictographic#  5.2  [2] (⚞..⚟)    THREE LINES CONVERGING RIGHT..THREE LINES CONVERGING LEFT +26A0..26A1    ; Extended_Pictographic#  4.0  [2] (⚠️..⚡)    warning..high voltage +26A2..26B1    ; Extended_Pictographic#  4.1 [16] (⚢..⚱️)    DOUBLED FEMALE SIGN..funeral urn +26B2          ; Extended_Pictographic#  5.0  [1] (⚲)       NEUTER +26B3..26BC    ; Extended_Pictographic#  5.1 [10] (⚳..⚼)    CERES..SESQUIQUADRATE +26BD..26BF    ; Extended_Pictographic#  5.2  [3] (⚽..⚿)    soccer ball..SQUARED KEY +26C0..26C3    ; Extended_Pictographic#  5.1  [4] (⛀..⛃)    WHITE DRAUGHTS MAN..BLACK DRAUGHTS KING +26C4..26CD    ; Extended_Pictographic#  5.2 [10] (⛄..⛍)    snowman without snow..DISABLED CAR +26CE          ; Extended_Pictographic#  6.0  [1] (⛎)       Ophiuchus +26CF..26E1    ; Extended_Pictographic#  5.2 [19] (⛏️..⛡)    pick..RESTRICTED LEFT ENTRY-2 +26E2          ; Extended_Pictographic#  6.0  [1] (⛢)       ASTRONOMICAL SYMBOL FOR URANUS +26E3          ; Extended_Pictographic#  5.2  [1] (⛣)       HEAVY CIRCLE WITH STROKE AND TWO DOTS ABOVE +26E4..26E7    ; Extended_Pictographic#  6.0  [4] (⛤..⛧)    PENTAGRAM..INVERTED PENTAGRAM +26E8..26FF    ; Extended_Pictographic#  5.2 [24] (⛨..⛿)    BLACK CROSS ON SHIELD..WHITE FLAG WITH HORIZONTAL MIDDLE BLACK STRIPE +2700          ; Extended_Pictographic#  7.0  [1] (✀)       BLACK SAFETY SCISSORS +2701..2704    ; Extended_Pictographic#  1.1  [4] (✁..✄)    UPPER BLADE SCISSORS..WHITE SCISSORS +2705          ; Extended_Pictographic#  6.0  [1] (✅)       check mark button +2708..2709    ; Extended_Pictographic#  1.1  [2] (✈️..✉️)    airplane..envelope +270A..270B    ; Extended_Pictographic#  6.0  [2] (✊..✋)    raised fist..raised hand +270C..2712    ; Extended_Pictographic#  1.1  [7] (✌️..✒️)    victory hand..black nib +2714          ; Extended_Pictographic#  1.1  [1] (✔️)       check mark +2716          ; Extended_Pictographic#  1.1  [1] (✖️)       multiplication sign +271D          ; Extended_Pictographic#  1.1  [1] (✝️)       latin cross +2721          ; Extended_Pictographic#  1.1  [1] (✡️)       star of David +2728          ; Extended_Pictographic#  6.0  [1] (✨)       sparkles +2733..2734    ; Extended_Pictographic#  1.1  [2] (✳️..✴️)    eight-spoked asterisk..eight-pointed star +2744          ; Extended_Pictographic#  1.1  [1] (❄️)       snowflake +2747          ; Extended_Pictographic#  1.1  [1] (❇️)       sparkle +274C          ; Extended_Pictographic#  6.0  [1] (❌)       cross mark +274E          ; Extended_Pictographic#  6.0  [1] (❎)       cross mark button +2753..2755    ; Extended_Pictographic#  6.0  [3] (❓..❕)    question mark..white exclamation mark +2757          ; Extended_Pictographic#  5.2  [1] (❗)       exclamation mark +2763..2767    ; Extended_Pictographic#  1.1  [5] (❣️..❧)    heart exclamation..ROTATED FLORAL HEART BULLET +2795..2797    ; Extended_Pictographic#  6.0  [3] (➕..➗)    plus sign..division sign +27A1          ; Extended_Pictographic#  1.1  [1] (➡️)       right arrow +27B0          ; Extended_Pictographic#  6.0  [1] (➰)       curly loop +27BF          ; Extended_Pictographic#  6.0  [1] (➿)       double curly loop +2934..2935    ; Extended_Pictographic#  3.2  [2] (⤴️..⤵️)    right arrow curving up..right arrow curving down +2B05..2B07    ; Extended_Pictographic#  4.0  [3] (⬅️..⬇️)    left arrow..down arrow +2B1B..2B1C    ; Extended_Pictographic#  5.1  [2] (⬛..⬜)    black large square..white large square +2B50          ; Extended_Pictographic#  5.1  [1] (⭐)       star +2B55          ; Extended_Pictographic#  5.2  [1] (⭕)       hollow red circle +3030          ; Extended_Pictographic#  1.1  [1] (〰️)       wavy dash +303D          ; Extended_Pictographic#  3.2  [1] (〽️)       part alternation mark +3297          ; Extended_Pictographic#  1.1  [1] (㊗️)       Japanese “congratulations” button +3299          ; Extended_Pictographic#  1.1  [1] (㊙️)       Japanese “secret” button +1F000..1F02B  ; Extended_Pictographic#  5.1 [44] (🀀..🀫)    MAHJONG TILE EAST WIND..MAHJONG TILE BACK +1F02C..1F02F  ; Extended_Pictographic#   NA  [4] (..)    <reserved-1F02C>..<reserved-1F02F> +1F030..1F093  ; Extended_Pictographic#  5.1[100] (🀰..🂓)    DOMINO TILE HORIZONTAL BACK..DOMINO TILE VERTICAL-06-06 +1F094..1F09F  ; Extended_Pictographic#   NA [12] (..)    <reserved-1F094>..<reserved-1F09F> +1F0A0..1F0AE  ; Extended_Pictographic#  6.0 [15] (🂠..🂮)    PLAYING CARD BACK..PLAYING CARD KING OF SPADES +1F0AF..1F0B0  ; Extended_Pictographic#   NA  [2] (..)    <reserved-1F0AF>..<reserved-1F0B0> +1F0B1..1F0BE  ; Extended_Pictographic#  6.0 [14] (🂱..🂾)    PLAYING CARD ACE OF HEARTS..PLAYING CARD KING OF HEARTS +1F0BF         ; Extended_Pictographic#  7.0  [1] (🂿)       PLAYING CARD RED JOKER +1F0C0         ; Extended_Pictographic#   NA  [1] ()       <reserved-1F0C0> +1F0C1..1F0CF  ; Extended_Pictographic#  6.0 [15] (🃁..🃏)    PLAYING CARD ACE OF DIAMONDS..joker +1F0D0         ; Extended_Pictographic#   NA  [1] ()       <reserved-1F0D0> +1F0D1..1F0DF  ; Extended_Pictographic#  6.0 [15] (🃑..🃟)    PLAYING CARD ACE OF CLUBS..PLAYING CARD WHITE JOKER +1F0E0..1F0F5  ; Extended_Pictographic#  7.0 [22] (🃠..🃵)    PLAYING CARD FOOL..PLAYING CARD TRUMP-21 +1F0F6..1F0FF  ; Extended_Pictographic#   NA [10] (..)    <reserved-1F0F6>..<reserved-1F0FF> +1F10D..1F10F  ; Extended_Pictographic#   NA  [3] (🄍..🄏)    <reserved-1F10D>..<reserved-1F10F> +1F12F         ; Extended_Pictographic# 11.0  [1] (🄯)       COPYLEFT SYMBOL +1F16C         ; Extended_Pictographic# 12.0  [1] (🅬)       RAISED MR SIGN +1F16D..1F16F  ; Extended_Pictographic#   NA  [3] (🅭..🅯)    <reserved-1F16D>..<reserved-1F16F> +1F170..1F171  ; Extended_Pictographic#  6.0  [2] (🅰️..🅱️)    A button (blood type)..B button (blood type) +1F17E         ; Extended_Pictographic#  6.0  [1] (🅾️)       O button (blood type) +1F17F         ; Extended_Pictographic#  5.2  [1] (🅿️)       P button +1F18E         ; Extended_Pictographic#  6.0  [1] (🆎)       AB button (blood type) +1F191..1F19A  ; Extended_Pictographic#  6.0 [10] (🆑..🆚)    CL button..VS button +1F1AD..1F1E5  ; Extended_Pictographic#   NA [57] (🆭..)    <reserved-1F1AD>..<reserved-1F1E5> +1F201..1F202  ; Extended_Pictographic#  6.0  [2] (🈁..🈂️)    Japanese “here” button..Japanese “service charge” button +1F203..1F20F  ; Extended_Pictographic#   NA [13] (..)    <reserved-1F203>..<reserved-1F20F> +1F21A         ; Extended_Pictographic#  5.2  [1] (🈚)       Japanese “free of charge” button +1F22F         ; Extended_Pictographic#  5.2  [1] (🈯)       Japanese “reserved” button +1F232..1F23A  ; Extended_Pictographic#  6.0  [9] (🈲..🈺)    Japanese “prohibited” button..Japanese “open for business” button +1F23C..1F23F  ; Extended_Pictographic#   NA  [4] (..)    <reserved-1F23C>..<reserved-1F23F> +1F249..1F24F  ; Extended_Pictographic#   NA  [7] (..)    <reserved-1F249>..<reserved-1F24F> +1F250..1F251  ; Extended_Pictographic#  6.0  [2] (🉐..🉑)    Japanese “bargain” button..Japanese “acceptable” button +1F252..1F25F  ; Extended_Pictographic#   NA [14] (..)    <reserved-1F252>..<reserved-1F25F> +1F260..1F265  ; Extended_Pictographic# 10.0  [6] (🉠..🉥)    ROUNDED SYMBOL FOR FU..ROUNDED SYMBOL FOR CAI +1F266..1F2FF  ; Extended_Pictographic#   NA[154] (..)    <reserved-1F266>..<reserved-1F2FF> +1F300..1F320  ; Extended_Pictographic#  6.0 [33] (🌀..🌠)    cyclone..shooting star +1F321..1F32C  ; Extended_Pictographic#  7.0 [12] (🌡️..🌬️)    thermometer..wind face +1F32D..1F32F  ; Extended_Pictographic#  8.0  [3] (🌭..🌯)    hot dog..burrito +1F330..1F335  ; Extended_Pictographic#  6.0  [6] (🌰..🌵)    chestnut..cactus +1F336         ; Extended_Pictographic#  7.0  [1] (🌶️)       hot pepper +1F337..1F37C  ; Extended_Pictographic#  6.0 [70] (🌷..🍼)    tulip..baby bottle +1F37D         ; Extended_Pictographic#  7.0  [1] (🍽️)       fork and knife with plate +1F37E..1F37F  ; Extended_Pictographic#  8.0  [2] (🍾..🍿)    bottle with popping cork..popcorn +1F380..1F393  ; Extended_Pictographic#  6.0 [20] (🎀..🎓)    ribbon..graduation cap +1F394..1F39F  ; Extended_Pictographic#  7.0 [12] (🎔..🎟️)    HEART WITH TIP ON THE LEFT..admission tickets +1F3A0..1F3C4  ; Extended_Pictographic#  6.0 [37] (🎠..🏄)    carousel horse..person surfing +1F3C5         ; Extended_Pictographic#  7.0  [1] (🏅)       sports medal +1F3C6..1F3CA  ; Extended_Pictographic#  6.0  [5] (🏆..🏊)    trophy..person swimming +1F3CB..1F3CE  ; Extended_Pictographic#  7.0  [4] (🏋️..🏎️)    person lifting weights..racing car +1F3CF..1F3D3  ; Extended_Pictographic#  8.0  [5] (🏏..🏓)    cricket game..ping pong +1F3D4..1F3DF  ; Extended_Pictographic#  7.0 [12] (🏔️..🏟️)    snow-capped mountain..stadium +1F3E0..1F3F0  ; Extended_Pictographic#  6.0 [17] (🏠..🏰)    house..castle +1F3F1..1F3F7  ; Extended_Pictographic#  7.0  [7] (🏱..🏷️)    WHITE PENNANT..label +1F3F8..1F3FA  ; Extended_Pictographic#  8.0  [3] (🏸..🏺)    badminton..amphora +1F400..1F43E  ; Extended_Pictographic#  6.0 [63] (🐀..🐾)    rat..paw prints +1F43F         ; Extended_Pictographic#  7.0  [1] (🐿️)       chipmunk +1F440         ; Extended_Pictographic#  6.0  [1] (👀)       eyes +1F441         ; Extended_Pictographic#  7.0  [1] (👁️)       eye +1F442..1F4F7  ; Extended_Pictographic#  6.0[182] (👂..📷)    ear..camera +1F4F8         ; Extended_Pictographic#  7.0  [1] (📸)       camera with flash +1F4F9..1F4FC  ; Extended_Pictographic#  6.0  [4] (📹..📼)    video camera..videocassette +1F4FD..1F4FE  ; Extended_Pictographic#  7.0  [2] (📽️..📾)    film projector..PORTABLE STEREO +1F4FF         ; Extended_Pictographic#  8.0  [1] (📿)       prayer beads +1F500..1F53D  ; Extended_Pictographic#  6.0 [62] (🔀..🔽)    shuffle tracks button..downwards button +1F546..1F54A  ; Extended_Pictographic#  7.0  [5] (🕆..🕊️)    WHITE LATIN CROSS..dove +1F54B..1F54F  ; Extended_Pictographic#  8.0  [5] (🕋..🕏)    kaaba..BOWL OF HYGIEIA +1F550..1F567  ; Extended_Pictographic#  6.0 [24] (🕐..🕧)    one o’clock..twelve-thirty +1F568..1F579  ; Extended_Pictographic#  7.0 [18] (🕨..🕹️)    RIGHT SPEAKER..joystick +1F57A         ; Extended_Pictographic#  9.0  [1] (🕺)       man dancing +1F57B..1F5A3  ; Extended_Pictographic#  7.0 [41] (🕻..🖣)    LEFT HAND TELEPHONE RECEIVER..BLACK DOWN POINTING BACKHAND INDEX +1F5A4         ; Extended_Pictographic#  9.0  [1] (🖤)       black heart +1F5A5..1F5FA  ; Extended_Pictographic#  7.0 [86] (🖥️..🗺️)    desktop computer..world map +1F5FB..1F5FF  ; Extended_Pictographic#  6.0  [5] (🗻..🗿)    mount fuji..moai +1F600         ; Extended_Pictographic#  6.1  [1] (😀)       grinning face +1F601..1F610  ; Extended_Pictographic#  6.0 [16] (😁..😐)    beaming face with smiling eyes..neutral face +1F611         ; Extended_Pictographic#  6.1  [1] (😑)       expressionless face +1F612..1F614  ; Extended_Pictographic#  6.0  [3] (😒..😔)    unamused face..pensive face +1F615         ; Extended_Pictographic#  6.1  [1] (😕)       confused face +1F616         ; Extended_Pictographic#  6.0  [1] (😖)       confounded face +1F617         ; Extended_Pictographic#  6.1  [1] (😗)       kissing face +1F618         ; Extended_Pictographic#  6.0  [1] (😘)       face blowing a kiss +1F619         ; Extended_Pictographic#  6.1  [1] (😙)       kissing face with smiling eyes +1F61A         ; Extended_Pictographic#  6.0  [1] (😚)       kissing face with closed eyes +1F61B         ; Extended_Pictographic#  6.1  [1] (😛)       face with tongue +1F61C..1F61E  ; Extended_Pictographic#  6.0  [3] (😜..😞)    winking face with tongue..disappointed face +1F61F         ; Extended_Pictographic#  6.1  [1] (😟)       worried face +1F620..1F625  ; Extended_Pictographic#  6.0  [6] (😠..😥)    angry face..sad but relieved face +1F626..1F627  ; Extended_Pictographic#  6.1  [2] (😦..😧)    frowning face with open mouth..anguished face +1F628..1F62B  ; Extended_Pictographic#  6.0  [4] (😨..😫)    fearful face..tired face +1F62C         ; Extended_Pictographic#  6.1  [1] (😬)       grimacing face +1F62D         ; Extended_Pictographic#  6.0  [1] (😭)       loudly crying face +1F62E..1F62F  ; Extended_Pictographic#  6.1  [2] (😮..😯)    face with open mouth..hushed face +1F630..1F633  ; Extended_Pictographic#  6.0  [4] (😰..😳)    anxious face with sweat..flushed face +1F634         ; Extended_Pictographic#  6.1  [1] (😴)       sleeping face +1F635..1F640  ; Extended_Pictographic#  6.0 [12] (😵..🙀)    dizzy face..weary cat +1F641..1F642  ; Extended_Pictographic#  7.0  [2] (🙁..🙂)    slightly frowning face..slightly smiling face +1F643..1F644  ; Extended_Pictographic#  8.0  [2] (🙃..🙄)    upside-down face..face with rolling eyes +1F645..1F64F  ; Extended_Pictographic#  6.0 [11] (🙅..🙏)    person gesturing NO..folded hands +1F680..1F6C5  ; Extended_Pictographic#  6.0 [70] (🚀..🛅)    rocket..left luggage +1F6C6..1F6CF  ; Extended_Pictographic#  7.0 [10] (🛆..🛏️)    TRIANGLE WITH ROUNDED CORNERS..bed +1F6D0         ; Extended_Pictographic#  8.0  [1] (🛐)       place of worship +1F6D1..1F6D2  ; Extended_Pictographic#  9.0  [2] (🛑..🛒)    stop sign..shopping cart +1F6D3..1F6D4  ; Extended_Pictographic# 10.0  [2] (🛓..🛔)    STUPA..PAGODA +1F6D5         ; Extended_Pictographic# 12.0  [1] (🛕)       hindu temple +1F6D6..1F6DF  ; Extended_Pictographic#   NA [10] (🛖..🛟)    <reserved-1F6D6>..<reserved-1F6DF> +1F6E0..1F6EC  ; Extended_Pictographic#  7.0 [13] (🛠️..🛬)    hammer and wrench..airplane arrival +1F6ED..1F6EF  ; Extended_Pictographic#   NA  [3] (..)    <reserved-1F6ED>..<reserved-1F6EF> +1F6F0..1F6F3  ; Extended_Pictographic#  7.0  [4] (🛰️..🛳️)    satellite..passenger ship +1F6F4..1F6F6  ; Extended_Pictographic#  9.0  [3] (🛴..🛶)    kick scooter..canoe +1F6F7..1F6F8  ; Extended_Pictographic# 10.0  [2] (🛷..🛸)    sled..flying saucer +1F6F9         ; Extended_Pictographic# 11.0  [1] (🛹)       skateboard +1F6FA         ; Extended_Pictographic# 12.0  [1] (🛺)       auto rickshaw +1F6FB..1F6FF  ; Extended_Pictographic#   NA  [5] (🛻..)    <reserved-1F6FB>..<reserved-1F6FF> +1F774..1F77F  ; Extended_Pictographic#   NA [12] (🝴..🝿)    <reserved-1F774>..<reserved-1F77F> +1F7D5..1F7D8  ; Extended_Pictographic# 11.0  [4] (🟕..🟘)    CIRCLED TRIANGLE..NEGATIVE CIRCLED SQUARE +1F7D9..1F7DF  ; Extended_Pictographic#   NA  [7] (🟙..)    <reserved-1F7D9>..<reserved-1F7DF> +1F7E0..1F7EB  ; Extended_Pictographic# 12.0 [12] (🟠..🟫)    orange circle..brown square +1F7EC..1F7FF  ; Extended_Pictographic#   NA [20] (..)    <reserved-1F7EC>..<reserved-1F7FF> +1F80C..1F80F  ; Extended_Pictographic#   NA  [4] (..)    <reserved-1F80C>..<reserved-1F80F> +1F848..1F84F  ; Extended_Pictographic#   NA  [8] (..)    <reserved-1F848>..<reserved-1F84F> +1F85A..1F85F  ; Extended_Pictographic#   NA  [6] (..)    <reserved-1F85A>..<reserved-1F85F> +1F888..1F88F  ; Extended_Pictographic#   NA  [8] (..)    <reserved-1F888>..<reserved-1F88F> +1F8AE..1F8FF  ; Extended_Pictographic#   NA [82] (..)    <reserved-1F8AE>..<reserved-1F8FF> +1F90C         ; Extended_Pictographic#   NA  [1] (🤌)       <reserved-1F90C> +1F90D..1F90F  ; Extended_Pictographic# 12.0  [3] (🤍..🤏)    white heart..pinching hand +1F910..1F918  ; Extended_Pictographic#  8.0  [9] (🤐..🤘)    zipper-mouth face..sign of the horns +1F919..1F91E  ; Extended_Pictographic#  9.0  [6] (🤙..🤞)    call me hand..crossed fingers +1F91F         ; Extended_Pictographic# 10.0  [1] (🤟)       love-you gesture +1F920..1F927  ; Extended_Pictographic#  9.0  [8] (🤠..🤧)    cowboy hat face..sneezing face +1F928..1F92F  ; Extended_Pictographic# 10.0  [8] (🤨..🤯)    face with raised eyebrow..exploding head +1F930         ; Extended_Pictographic#  9.0  [1] (🤰)       pregnant woman +1F931..1F932  ; Extended_Pictographic# 10.0  [2] (🤱..🤲)    breast-feeding..palms up together +1F933..1F93A  ; Extended_Pictographic#  9.0  [8] (🤳..🤺)    selfie..person fencing +1F93C..1F93E  ; Extended_Pictographic#  9.0  [3] (🤼..🤾)    people wrestling..person playing handball +1F93F         ; Extended_Pictographic# 12.0  [1] (🤿)       diving mask +1F940..1F945  ; Extended_Pictographic#  9.0  [6] (🥀..🥅)    wilted flower..goal net +1F947..1F94B  ; Extended_Pictographic#  9.0  [5] (🥇..🥋)    1st place medal..martial arts uniform +1F94C         ; Extended_Pictographic# 10.0  [1] (🥌)       curling stone +1F94D..1F94F  ; Extended_Pictographic# 11.0  [3] (🥍..🥏)    lacrosse..flying disc +1F950..1F95E  ; Extended_Pictographic#  9.0 [15] (🥐..🥞)    croissant..pancakes +1F95F..1F96B  ; Extended_Pictographic# 10.0 [13] (🥟..🥫)    dumpling..canned food +1F96C..1F970  ; Extended_Pictographic# 11.0  [5] (🥬..🥰)    leafy green..smiling face with hearts +1F971         ; Extended_Pictographic# 12.0  [1] (🥱)       yawning face +1F972         ; Extended_Pictographic#   NA  [1] (🥲)       <reserved-1F972> +1F973..1F976  ; Extended_Pictographic# 11.0  [4] (🥳..🥶)    partying face..cold face +1F977..1F979  ; Extended_Pictographic#   NA  [3] (🥷..🥹)    <reserved-1F977>..<reserved-1F979> +1F97A         ; Extended_Pictographic# 11.0  [1] (🥺)       pleading face +1F97B         ; Extended_Pictographic# 12.0  [1] (🥻)       sari +1F97C..1F97F  ; Extended_Pictographic# 11.0  [4] (🥼..🥿)    lab coat..flat shoe +1F980..1F984  ; Extended_Pictographic#  8.0  [5] (🦀..🦄)    crab..unicorn +1F985..1F991  ; Extended_Pictographic#  9.0 [13] (🦅..🦑)    eagle..squid +1F992..1F997  ; Extended_Pictographic# 10.0  [6] (🦒..🦗)    giraffe..cricket +1F998..1F9A2  ; Extended_Pictographic# 11.0 [11] (🦘..🦢)    kangaroo..swan +1F9A3..1F9A4  ; Extended_Pictographic#   NA  [2] (🦣..🦤)    <reserved-1F9A3>..<reserved-1F9A4> +1F9A5..1F9AA  ; Extended_Pictographic# 12.0  [6] (🦥..🦪)    sloth..oyster +1F9AB..1F9AD  ; Extended_Pictographic#   NA  [3] (🦫..🦭)    <reserved-1F9AB>..<reserved-1F9AD> +1F9AE..1F9AF  ; Extended_Pictographic# 12.0  [2] (🦮..🦯)    guide dog..probing cane +1F9B0..1F9B9  ; Extended_Pictographic# 11.0 [10] (🦰..🦹)    red hair..supervillain +1F9BA..1F9BF  ; Extended_Pictographic# 12.0  [6] (🦺..🦿)    safety vest..mechanical leg +1F9C0         ; Extended_Pictographic#  8.0  [1] (🧀)       cheese wedge +1F9C1..1F9C2  ; Extended_Pictographic# 11.0  [2] (🧁..🧂)    cupcake..salt +1F9C3..1F9CA  ; Extended_Pictographic# 12.0  [8] (🧃..🧊)    beverage box..ice cube +1F9CB..1F9CC  ; Extended_Pictographic#   NA  [2] (🧋..🧌)    <reserved-1F9CB>..<reserved-1F9CC> +1F9CD..1F9CF  ; Extended_Pictographic# 12.0  [3] (🧍..🧏)    person standing..deaf person +1F9D0..1F9E6  ; Extended_Pictographic# 10.0 [23] (🧐..🧦)    face with monocle..socks +1F9E7..1F9FF  ; Extended_Pictographic# 11.0 [25] (🧧..🧿)    red envelope..nazar amulet +1FA00..1FA53  ; Extended_Pictographic# 12.0 [84] (🨀..🩓)    NEUTRAL CHESS KING..BLACK CHESS KNIGHT-BISHOP +1FA54..1FA5F  ; Extended_Pictographic#   NA [12] (..)    <reserved-1FA54>..<reserved-1FA5F> +1FA60..1FA6D  ; Extended_Pictographic# 11.0 [14] (🩠..🩭)    XIANGQI RED GENERAL..XIANGQI BLACK SOLDIER +1FA6E..1FA6F  ; Extended_Pictographic#   NA  [2] (..)    <reserved-1FA6E>..<reserved-1FA6F> +1FA70..1FA73  ; Extended_Pictographic# 12.0  [4] (🩰..🩳)    ballet shoes..shorts +1FA74..1FA77  ; Extended_Pictographic#   NA  [4] (🩴..🩷)    <reserved-1FA74>..<reserved-1FA77> +1FA78..1FA7A  ; Extended_Pictographic# 12.0  [3] (🩸..🩺)    drop of blood..stethoscope +1FA7B..1FA7F  ; Extended_Pictographic#   NA  [5] (🩻..)    <reserved-1FA7B>..<reserved-1FA7F> +1FA80..1FA82  ; Extended_Pictographic# 12.0  [3] (🪀..🪂)    yo-yo..parachute +1FA83..1FA8F  ; Extended_Pictographic#   NA [13] (🪃..)    <reserved-1FA83>..<reserved-1FA8F> +1FA90..1FA95  ; Extended_Pictographic# 12.0  [6] (🪐..🪕)    ringed planet..banjo +1FA96..1FFFD  ; Extended_Pictographic#   NA[1384] (🪖..)   <reserved-1FA96>..<reserved-1FFFD> + +# Total elements: 3793 + +#EOF diff --git a/lib/pleroma/emoji.ex b/lib/pleroma/emoji.ex index bafad2ae9..abfd49aaa 100644 --- a/lib/pleroma/emoji.ex +++ b/lib/pleroma/emoji.ex @@ -98,4 +98,35 @@ defmodule Pleroma.Emoji do    defp update_emojis(emojis) do      :ets.insert(@ets, emojis)    end + +  @external_resource "lib/pleroma/emoji-data.txt" + +  emojis = +    @external_resource +    |> File.read!() +    |> String.split("\n") +    |> Enum.filter(fn line -> line != "" and not String.starts_with?(line, "#") end) +    |> Enum.map(fn line -> +      line +      |> String.split(";", parts: 2) +      |> hd() +      |> String.trim() +      |> String.split("..") +      |> case do +        [number] -> +          <<String.to_integer(number, 16)::utf8>> + +        [first, last] -> +          String.to_integer(first, 16)..String.to_integer(last, 16) +          |> Enum.map(&<<&1::utf8>>) +      end +    end) +    |> List.flatten() +    |> Enum.uniq() + +  for emoji <- emojis do +    def is_unicode_emoji?(unquote(emoji)), do: true +  end + +  def is_unicode_emoji?(_), do: false  end diff --git a/lib/pleroma/object/containment.ex b/lib/pleroma/object/containment.ex index a1f9c1250..25aa32f60 100644 --- a/lib/pleroma/object/containment.ex +++ b/lib/pleroma/object/containment.ex @@ -64,6 +64,8 @@ defmodule Pleroma.Object.Containment do    def contain_origin(id, %{"attributedTo" => actor} = params),      do: contain_origin(id, Map.put(params, "actor", actor)) +  def contain_origin(_id, _data), do: :error +    def contain_origin_from_id(id, %{"id" => other_id} = _params) when is_binary(other_id) do      id_uri = URI.parse(id)      other_uri = URI.parse(other_id) diff --git a/lib/pleroma/plugs/rate_limiter.ex b/lib/pleroma/plugs/rate_limiter.ex deleted file mode 100644 index 31388f574..000000000 --- a/lib/pleroma/plugs/rate_limiter.ex +++ /dev/null @@ -1,131 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.RateLimiter do -  @moduledoc """ - -  ## Configuration - -  A keyword list of rate limiters where a key is a limiter name and value is the limiter configuration. The basic configuration is a tuple where: - -  * The first element: `scale` (Integer). The time scale in milliseconds. -  * The second element: `limit` (Integer). How many requests to limit in the time scale provided. - -  It is also possible to have different limits for unauthenticated and authenticated users: the keyword value must be a list of two tuples where the first one is a config for unauthenticated users and the second one is for authenticated. - -  To disable a limiter set its value to `nil`. - -  ### Example - -      config :pleroma, :rate_limit, -        one: {1000, 10}, -        two: [{10_000, 10}, {10_000, 50}], -        foobar: nil - -  Here we have three limiters: - -  * `one` which is not over 10req/1s -  * `two` which has two limits: 10req/10s for unauthenticated users and 50req/10s for authenticated users -  * `foobar` which is disabled - -  ## Usage - -  AllowedSyntax: - -      plug(Pleroma.Plugs.RateLimiter, :limiter_name) -      plug(Pleroma.Plugs.RateLimiter, {:limiter_name, options}) - -  Allowed options: - -      * `bucket_name` overrides bucket name (e.g. to have a separate limit for a set of actions) -      * `params` appends values of specified request params (e.g. ["id"]) to bucket name - -  Inside a controller: - -      plug(Pleroma.Plugs.RateLimiter, :one when action == :one) -      plug(Pleroma.Plugs.RateLimiter, :two when action in [:two, :three]) - -      plug( -        Pleroma.Plugs.RateLimiter, -        {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]} -        when action in ~w(fav_status unfav_status)a -      ) - -  or inside a router pipeline: - -      pipeline :api do -        ... -        plug(Pleroma.Plugs.RateLimiter, :one) -        ... -      end -  """ -  import Pleroma.Web.TranslationHelpers -  import Plug.Conn - -  alias Pleroma.User - -  def init(limiter_name) when is_atom(limiter_name) do -    init({limiter_name, []}) -  end - -  def init({limiter_name, opts}) do -    case Pleroma.Config.get([:rate_limit, limiter_name]) do -      nil -> nil -      config -> {limiter_name, config, opts} -    end -  end - -  # Do not limit if there is no limiter configuration -  def call(conn, nil), do: conn - -  def call(conn, settings) do -    case check_rate(conn, settings) do -      {:ok, _count} -> -        conn - -      {:error, _count} -> -        render_throttled_error(conn) -    end -  end - -  defp bucket_name(conn, limiter_name, opts) do -    bucket_name = opts[:bucket_name] || limiter_name - -    if params_names = opts[:params] do -      params_values = for p <- Enum.sort(params_names), do: conn.params[p] -      Enum.join([bucket_name] ++ params_values, ":") -    else -      bucket_name -    end -  end - -  defp check_rate( -         %{assigns: %{user: %User{id: user_id}}} = conn, -         {limiter_name, [_, {scale, limit}], opts} -       ) do -    bucket_name = bucket_name(conn, limiter_name, opts) -    ExRated.check_rate("#{bucket_name}:#{user_id}", scale, limit) -  end - -  defp check_rate(conn, {limiter_name, [{scale, limit} | _], opts}) do -    bucket_name = bucket_name(conn, limiter_name, opts) -    ExRated.check_rate("#{bucket_name}:#{ip(conn)}", scale, limit) -  end - -  defp check_rate(conn, {limiter_name, {scale, limit}, opts}) do -    check_rate(conn, {limiter_name, [{scale, limit}, {scale, limit}], opts}) -  end - -  def ip(%{remote_ip: remote_ip}) do -    remote_ip -    |> Tuple.to_list() -    |> Enum.join(".") -  end - -  defp render_throttled_error(conn) do -    conn -    |> render_error(:too_many_requests, "Throttled") -    |> halt() -  end -end diff --git a/lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex b/lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex new file mode 100644 index 000000000..187582ede --- /dev/null +++ b/lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex @@ -0,0 +1,44 @@ +defmodule Pleroma.Plugs.RateLimiter.LimiterSupervisor do +  use DynamicSupervisor + +  import Cachex.Spec + +  def start_link(init_arg) do +    DynamicSupervisor.start_link(__MODULE__, init_arg, name: __MODULE__) +  end + +  def add_limiter(limiter_name, expiration) do +    {:ok, _pid} = +      DynamicSupervisor.start_child( +        __MODULE__, +        %{ +          id: String.to_atom("rl_#{limiter_name}"), +          start: +            {Cachex, :start_link, +             [ +               limiter_name, +               [ +                 expiration: +                   expiration( +                     default: expiration, +                     interval: check_interval(expiration), +                     lazy: true +                   ) +               ] +             ]} +        } +      ) +  end + +  @impl true +  def init(_init_arg) do +    DynamicSupervisor.init(strategy: :one_for_one) +  end + +  defp check_interval(exp) do +    (exp / 2) +    |> Kernel.trunc() +    |> Kernel.min(5000) +    |> Kernel.max(1) +  end +end diff --git a/lib/pleroma/plugs/rate_limiter/rate_limiter.ex b/lib/pleroma/plugs/rate_limiter/rate_limiter.ex new file mode 100644 index 000000000..d720508c8 --- /dev/null +++ b/lib/pleroma/plugs/rate_limiter/rate_limiter.ex @@ -0,0 +1,227 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.RateLimiter do +  @moduledoc """ + +  ## Configuration + +  A keyword list of rate limiters where a key is a limiter name and value is the limiter configuration. The basic configuration is a tuple where: + +  * The first element: `scale` (Integer). The time scale in milliseconds. +  * The second element: `limit` (Integer). How many requests to limit in the time scale provided. + +  It is also possible to have different limits for unauthenticated and authenticated users: the keyword value must be a list of two tuples where the first one is a config for unauthenticated users and the second one is for authenticated. + +  To disable a limiter set its value to `nil`. + +  ### Example + +      config :pleroma, :rate_limit, +        one: {1000, 10}, +        two: [{10_000, 10}, {10_000, 50}], +        foobar: nil + +  Here we have three limiters: + +  * `one` which is not over 10req/1s +  * `two` which has two limits: 10req/10s for unauthenticated users and 50req/10s for authenticated users +  * `foobar` which is disabled + +  ## Usage + +  AllowedSyntax: + +      plug(Pleroma.Plugs.RateLimiter, name: :limiter_name) +      plug(Pleroma.Plugs.RateLimiter, options)   # :name is a required option + +  Allowed options: + +      * `name` required, always used to fetch the limit values from the config +      * `bucket_name` overrides name for counting purposes (e.g. to have a separate limit for a set of actions) +      * `params` appends values of specified request params (e.g. ["id"]) to bucket name + +  Inside a controller: + +      plug(Pleroma.Plugs.RateLimiter, [name: :one] when action == :one) +      plug(Pleroma.Plugs.RateLimiter, [name: :two] when action in [:two, :three]) + +      plug( +        Pleroma.Plugs.RateLimiter, +        [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]] +        when action in ~w(fav_status unfav_status)a +      ) + +  or inside a router pipeline: + +      pipeline :api do +        ... +        plug(Pleroma.Plugs.RateLimiter, name: :one) +        ... +      end +  """ +  import Pleroma.Web.TranslationHelpers +  import Plug.Conn + +  alias Pleroma.Plugs.RateLimiter.LimiterSupervisor +  alias Pleroma.User + +  def init(opts) do +    limiter_name = Keyword.get(opts, :name) + +    case Pleroma.Config.get([:rate_limit, limiter_name]) do +      nil -> +        nil + +      config -> +        name_root = Keyword.get(opts, :bucket_name, limiter_name) + +        %{ +          name: name_root, +          limits: config, +          opts: opts +        } +    end +  end + +  # Do not limit if there is no limiter configuration +  def call(conn, nil), do: conn + +  def call(conn, settings) do +    settings +    |> incorporate_conn_info(conn) +    |> check_rate() +    |> case do +      {:ok, _count} -> +        conn + +      {:error, _count} -> +        render_throttled_error(conn) +    end +  end + +  def inspect_bucket(conn, name_root, settings) do +    settings = +      settings +      |> incorporate_conn_info(conn) + +    bucket_name = make_bucket_name(%{settings | name: name_root}) +    key_name = make_key_name(settings) +    limit = get_limits(settings) + +    case Cachex.get(bucket_name, key_name) do +      {:error, :no_cache} -> +        {:err, :not_found} + +      {:ok, nil} -> +        {0, limit} + +      {:ok, value} -> +        {value, limit - value} +    end +  end + +  defp check_rate(settings) do +    bucket_name = make_bucket_name(settings) +    key_name = make_key_name(settings) +    limit = get_limits(settings) + +    case Cachex.get_and_update(bucket_name, key_name, &increment_value(&1, limit)) do +      {:commit, value} -> +        {:ok, value} + +      {:ignore, value} -> +        {:error, value} + +      {:error, :no_cache} -> +        initialize_buckets(settings) +        check_rate(settings) +    end +  end + +  defp increment_value(nil, _limit), do: {:commit, 1} + +  defp increment_value(val, limit) when val >= limit, do: {:ignore, val} + +  defp increment_value(val, _limit), do: {:commit, val + 1} + +  defp incorporate_conn_info(settings, %{assigns: %{user: %User{id: user_id}}, params: params}) do +    Map.merge(settings, %{ +      mode: :user, +      conn_params: params, +      conn_info: "#{user_id}" +    }) +  end + +  defp incorporate_conn_info(settings, %{params: params} = conn) do +    Map.merge(settings, %{ +      mode: :anon, +      conn_params: params, +      conn_info: "#{ip(conn)}" +    }) +  end + +  defp ip(%{remote_ip: remote_ip}) do +    remote_ip +    |> Tuple.to_list() +    |> Enum.join(".") +  end + +  defp render_throttled_error(conn) do +    conn +    |> render_error(:too_many_requests, "Throttled") +    |> halt() +  end + +  defp make_key_name(settings) do +    "" +    |> attach_params(settings) +    |> attach_identity(settings) +  end + +  defp get_scale(_, {scale, _}), do: scale + +  defp get_scale(:anon, [{scale, _}, {_, _}]), do: scale + +  defp get_scale(:user, [{_, _}, {scale, _}]), do: scale + +  defp get_limits(%{limits: {_scale, limit}}), do: limit + +  defp get_limits(%{mode: :user, limits: [_, {_, limit}]}), do: limit + +  defp get_limits(%{limits: [{_, limit}, _]}), do: limit + +  defp make_bucket_name(%{mode: :user, name: name_root}), +    do: user_bucket_name(name_root) + +  defp make_bucket_name(%{mode: :anon, name: name_root}), +    do: anon_bucket_name(name_root) + +  defp attach_params(input, %{conn_params: conn_params, opts: opts}) do +    param_string = +      opts +      |> Keyword.get(:params, []) +      |> Enum.sort() +      |> Enum.map(&Map.get(conn_params, &1, "")) +      |> Enum.join(":") + +    "#{input}#{param_string}" +  end + +  defp initialize_buckets(%{name: _name, limits: nil}), do: :ok + +  defp initialize_buckets(%{name: name, limits: limits}) do +    LimiterSupervisor.add_limiter(anon_bucket_name(name), get_scale(:anon, limits)) +    LimiterSupervisor.add_limiter(user_bucket_name(name), get_scale(:user, limits)) +  end + +  defp attach_identity(base, %{mode: :user, conn_info: conn_info}), +    do: "user:#{base}:#{conn_info}" + +  defp attach_identity(base, %{mode: :anon, conn_info: conn_info}), +    do: "ip:#{base}:#{conn_info}" + +  defp user_bucket_name(name_root), do: "user:#{name_root}" |> String.to_atom() +  defp anon_bucket_name(name_root), do: "anon:#{name_root}" |> String.to_atom() +end diff --git a/lib/pleroma/plugs/rate_limiter/supervisor.ex b/lib/pleroma/plugs/rate_limiter/supervisor.ex new file mode 100644 index 000000000..9672f7876 --- /dev/null +++ b/lib/pleroma/plugs/rate_limiter/supervisor.ex @@ -0,0 +1,16 @@ +defmodule Pleroma.Plugs.RateLimiter.Supervisor do +  use Supervisor + +  def start_link(opts) do +    Supervisor.start_link(__MODULE__, opts, name: __MODULE__) +  end + +  def init(_args) do +    children = [ +      Pleroma.Plugs.RateLimiter.LimiterSupervisor +    ] + +    opts = [strategy: :one_for_one, name: Pleroma.Web.Streamer.Supervisor] +    Supervisor.init(children, opts) +  end +end diff --git a/lib/pleroma/plugs/static_fe_plug.ex b/lib/pleroma/plugs/static_fe_plug.ex new file mode 100644 index 000000000..b3fb3c582 --- /dev/null +++ b/lib/pleroma/plugs/static_fe_plug.ex @@ -0,0 +1,26 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.StaticFEPlug do +  import Plug.Conn +  alias Pleroma.Web.StaticFE.StaticFEController + +  def init(options), do: options + +  def call(conn, _) do +    if enabled?() and accepts_html?(conn) do +      conn +      |> StaticFEController.call(:show) +      |> halt() +    else +      conn +    end +  end + +  defp enabled?, do: Pleroma.Config.get([:static_fe, :enabled], false) + +  defp accepts_html?(conn) do +    conn |> get_req_header("accept") |> List.first() |> String.contains?("text/html") +  end +end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 65dd251f3..d0c014e9d 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -322,6 +322,32 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      end    end +  def react_with_emoji(user, object, emoji, options \\ []) do +    with local <- Keyword.get(options, :local, true), +         activity_id <- Keyword.get(options, :activity_id, nil), +         Pleroma.Emoji.is_unicode_emoji?(emoji), +         reaction_data <- make_emoji_reaction_data(user, object, emoji, activity_id), +         {:ok, activity} <- insert(reaction_data, local), +         {:ok, object} <- add_emoji_reaction_to_object(activity, object), +         :ok <- maybe_federate(activity) do +      {:ok, activity, object} +    end +  end + +  def unreact_with_emoji(user, reaction_id, options \\ []) do +    with local <- Keyword.get(options, :local, true), +         activity_id <- Keyword.get(options, :activity_id, nil), +         user_ap_id <- user.ap_id, +         %Activity{actor: ^user_ap_id} = reaction_activity <- Activity.get_by_ap_id(reaction_id), +         object <- Object.normalize(reaction_activity), +         unreact_data <- make_undo_data(user, reaction_activity, activity_id), +         {:ok, activity} <- insert(unreact_data, local), +         {:ok, object} <- remove_emoji_reaction_from_object(reaction_activity, object), +         :ok <- maybe_federate(activity) do +      {:ok, activity, object} +    end +  end +    # TODO: This is weird, maybe we shouldn't check here if we can make the activity.    def like(          %User{ap_id: ap_id} = user, diff --git a/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex b/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex new file mode 100644 index 000000000..8b36c1021 --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex @@ -0,0 +1,101 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do +  alias Pleroma.Config +  alias Pleroma.User +  alias Pleroma.Web.ActivityPub.MRF + +  require Pleroma.Constants + +  @moduledoc "Filter activities depending on their age" +  @behaviour MRF + +  defp check_date(%{"published" => published} = message) do +    with %DateTime{} = now <- DateTime.utc_now(), +         {:ok, %DateTime{} = then, _} <- DateTime.from_iso8601(published), +         max_ttl <- Config.get([:mrf_object_age, :threshold]), +         {:ttl, false} <- {:ttl, DateTime.diff(now, then) > max_ttl} do +      {:ok, message} +    else +      {:ttl, true} -> +        {:reject, nil} + +      e -> +        {:error, e} +    end +  end + +  defp check_reject(message, actions) do +    if :reject in actions do +      {:reject, nil} +    else +      {:ok, message} +    end +  end + +  defp check_delist(message, actions) do +    if :delist in actions do +      with %User{} = user <- User.get_cached_by_ap_id(message["actor"]) do +        to = List.delete(message["to"], Pleroma.Constants.as_public()) ++ [user.follower_address] +        cc = List.delete(message["cc"], user.follower_address) ++ [Pleroma.Constants.as_public()] + +        message = +          message +          |> Map.put("to", to) +          |> Map.put("cc", cc) + +        {:ok, message} +      else +        # Unhandleable error: somebody is messing around, just drop the message. +        _e -> +          {:reject, nil} +      end +    else +      {:ok, message} +    end +  end + +  defp check_strip_followers(message, actions) do +    if :strip_followers in actions do +      with %User{} = user <- User.get_cached_by_ap_id(message["actor"]) do +        to = List.delete(message["to"], user.follower_address) +        cc = List.delete(message["cc"], user.follower_address) + +        message = +          message +          |> Map.put("to", to) +          |> Map.put("cc", cc) + +        {:ok, message} +      else +        # Unhandleable error: somebody is messing around, just drop the message. +        _e -> +          {:reject, nil} +      end +    else +      {:ok, message} +    end +  end + +  @impl true +  def filter(%{"type" => "Create", "published" => _} = message) do +    with actions <- Config.get([:mrf_object_age, :actions]), +         {:reject, _} <- check_date(message), +         {:ok, message} <- check_reject(message, actions), +         {:ok, message} <- check_delist(message, actions), +         {:ok, message} <- check_strip_followers(message, actions) do +      {:ok, message} +    else +      # check_date() is allowed to short-circuit the pipeline +      e -> e +    end +  end + +  @impl true +  def filter(message), do: {:ok, message} + +  @impl true +  def describe, do: {:ok, %{}} +end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 91a164eff..15612545b 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -566,6 +566,34 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      end    end +  @misskey_reactions %{ +    "like" => "👍", +    "love" => "❤️", +    "laugh" => "😆", +    "hmm" => "🤔", +    "surprise" => "😮", +    "congrats" => "🎉", +    "angry" => "💢", +    "confused" => "😥", +    "rip" => "😇", +    "pudding" => "🍮", +    "star" => "⭐" +  } + +  @doc "Rewrite misskey likes into EmojiReactions" +  def handle_incoming( +        %{ +          "type" => "Like", +          "_misskey_reaction" => reaction +        } = data, +        options +      ) do +    data +    |> Map.put("type", "EmojiReaction") +    |> Map.put("content", @misskey_reactions[reaction] || reaction) +    |> handle_incoming(options) +  end +    def handle_incoming(          %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data,          _options @@ -581,6 +609,27 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    end    def handle_incoming( +        %{ +          "type" => "EmojiReaction", +          "object" => object_id, +          "actor" => _actor, +          "id" => id, +          "content" => emoji +        } = data, +        _options +      ) do +    with actor <- Containment.get_actor(data), +         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor), +         {:ok, object} <- get_obj_helper(object_id), +         {:ok, activity, _object} <- +           ActivityPub.react_with_emoji(actor, object, emoji, activity_id: id, local: false) do +      {:ok, activity} +    else +      _e -> :error +    end +  end + +  def handle_incoming(          %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,          _options        ) do @@ -718,6 +767,28 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    def handle_incoming(          %{            "type" => "Undo", +          "object" => %{"type" => "EmojiReaction", "id" => reaction_activity_id}, +          "actor" => _actor, +          "id" => id +        } = data, +        _options +      ) do +    with actor <- Containment.get_actor(data), +         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor), +         {:ok, activity, _} <- +           ActivityPub.unreact_with_emoji(actor, reaction_activity_id, +             activity_id: id, +             local: false +           ) do +      {:ok, activity} +    else +      _e -> :error +    end +  end + +  def handle_incoming( +        %{ +          "type" => "Undo",            "object" => %{"type" => "Block", "object" => blocked},            "actor" => blocker,            "id" => id @@ -1048,7 +1119,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      Map.put(object, "attachment", attachments)    end -  defp strip_internal_fields(object) do +  def strip_internal_fields(object) do      object      |> Map.drop(Pleroma.Constants.object_internal_fields())    end diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index d812fd734..c45662359 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -11,6 +11,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do    alias Pleroma.Repo    alias Pleroma.User    alias Pleroma.Web +  alias Pleroma.Web.ActivityPub.ActivityPub    alias Pleroma.Web.ActivityPub.Visibility    alias Pleroma.Web.AdminAPI.AccountView    alias Pleroma.Web.Endpoint @@ -255,6 +256,16 @@ defmodule Pleroma.Web.ActivityPub.Utils do      |> Repo.one()    end +  @doc """ +  Returns like activities targeting an object +  """ +  def get_object_likes(%{data: %{"id" => id}}) do +    id +    |> Activity.Queries.by_object_id() +    |> Activity.Queries.by_type("Like") +    |> Repo.all() +  end +    @spec make_like_data(User.t(), map(), String.t()) :: map()    def make_like_data(          %User{ap_id: ap_id} = actor, @@ -286,13 +297,30 @@ defmodule Pleroma.Web.ActivityPub.Utils do      |> maybe_put("id", activity_id)    end +  def make_emoji_reaction_data(user, object, emoji, activity_id) do +    make_like_data(user, object, activity_id) +    |> Map.put("type", "EmojiReaction") +    |> Map.put("content", emoji) +  end +    @spec update_element_in_object(String.t(), list(any), Object.t()) ::            {:ok, Object.t()} | {:error, Ecto.Changeset.t()}    def update_element_in_object(property, element, object) do +    length = +      if is_map(element) do +        element +        |> Map.values() +        |> List.flatten() +        |> length() +      else +        element +        |> length() +      end +      data =        Map.merge(          object.data, -        %{"#{property}_count" => length(element), "#{property}s" => element} +        %{"#{property}_count" => length, "#{property}s" => element}        )      object @@ -300,6 +328,38 @@ defmodule Pleroma.Web.ActivityPub.Utils do      |> Object.update_and_set_cache()    end +  @spec add_emoji_reaction_to_object(Activity.t(), Object.t()) :: +          {:ok, Object.t()} | {:error, Ecto.Changeset.t()} + +  def add_emoji_reaction_to_object( +        %Activity{data: %{"content" => emoji, "actor" => actor}}, +        object +      ) do +    reactions = object.data["reactions"] || %{} +    emoji_actors = reactions[emoji] || [] +    new_emoji_actors = [actor | emoji_actors] |> Enum.uniq() +    new_reactions = Map.put(reactions, emoji, new_emoji_actors) +    update_element_in_object("reaction", new_reactions, object) +  end + +  def remove_emoji_reaction_from_object( +        %Activity{data: %{"content" => emoji, "actor" => actor}}, +        object +      ) do +    reactions = object.data["reactions"] || %{} +    emoji_actors = reactions[emoji] || [] +    new_emoji_actors = List.delete(emoji_actors, actor) + +    new_reactions = +      if new_emoji_actors == [] do +        Map.delete(reactions, emoji) +      else +        Map.put(reactions, emoji, new_emoji_actors) +      end + +    update_element_in_object("reaction", new_reactions, object) +  end +    @spec add_like_to_object(Activity.t(), Object.t()) ::            {:ok, Object.t()} | {:error, Ecto.Changeset.t()}    def add_like_to_object(%Activity{data: %{"actor" => actor}}, object) do @@ -397,6 +457,19 @@ defmodule Pleroma.Web.ActivityPub.Utils do      |> Repo.one()    end +  def get_latest_reaction(internal_activity_id, %{ap_id: ap_id}, emoji) do +    %{data: %{"object" => object_ap_id}} = Activity.get_by_id(internal_activity_id) + +    "EmojiReaction" +    |> Activity.Queries.by_type() +    |> where(actor: ^ap_id) +    |> where([activity], fragment("?->>'content' = ?", activity.data, ^emoji)) +    |> Activity.Queries.by_object_id(object_ap_id) +    |> order_by([activity], fragment("? desc nulls last", activity.id)) +    |> limit(1) +    |> Repo.one() +  end +    #### Announce-related helpers    @doc """ @@ -489,6 +562,25 @@ defmodule Pleroma.Web.ActivityPub.Utils do      |> maybe_put("id", activity_id)    end +  def make_undo_data( +        %User{ap_id: actor, follower_address: follower_address}, +        %Activity{ +          data: %{"id" => undone_activity_id, "context" => context}, +          actor: undone_activity_actor +        }, +        activity_id \\ nil +      ) do +    %{ +      "type" => "Undo", +      "actor" => actor, +      "object" => undone_activity_id, +      "to" => [follower_address, undone_activity_actor], +      "cc" => [Pleroma.Constants.as_public()], +      "context" => context +    } +    |> maybe_put("id", activity_id) +  end +    @spec add_announce_to_object(Activity.t(), Object.t()) ::            {:ok, Object.t()} | {:error, Ecto.Changeset.t()}    def add_announce_to_object( @@ -615,26 +707,31 @@ defmodule Pleroma.Web.ActivityPub.Utils do    def make_flag_data(_, _), do: %{}    defp build_flag_object(%{account: account, statuses: statuses} = _) do -    [account.ap_id] ++ -      Enum.map(statuses || [], fn act -> -        id = -          case act do -            %Activity{} = act -> act.data["id"] -            act when is_map(act) -> act["id"] -            act when is_binary(act) -> act -          end +    [account.ap_id] ++ build_flag_object(%{statuses: statuses}) +  end + +  defp build_flag_object(%{statuses: statuses}) do +    Enum.map(statuses || [], &build_flag_object/1) +  end -        activity = Activity.get_by_ap_id_with_object(id) -        actor = User.get_by_ap_id(activity.object.data["actor"]) +  defp build_flag_object(act) when is_map(act) or is_binary(act) do +    id = +      case act do +        %Activity{} = act -> act.data["id"] +        act when is_map(act) -> act["id"] +        act when is_binary(act) -> act +      end -        %{ -          "type" => "Note", -          "id" => activity.data["id"], -          "content" => activity.object.data["content"], -          "published" => activity.object.data["published"], -          "actor" => AccountView.render("show.json", %{user: actor}) -        } -      end) +    activity = Activity.get_by_ap_id_with_object(id) +    actor = User.get_by_ap_id(activity.object.data["actor"]) + +    %{ +      "type" => "Note", +      "id" => activity.data["id"], +      "content" => activity.object.data["content"], +      "published" => activity.object.data["published"], +      "actor" => AccountView.render("show.json", %{user: actor}) +    }    end    defp build_flag_object(_), do: [] @@ -679,6 +776,94 @@ defmodule Pleroma.Web.ActivityPub.Utils do    end    #### Report-related helpers +  def get_reports(params, page, page_size) do +    params = +      params +      |> Map.put("type", "Flag") +      |> Map.put("skip_preload", true) +      |> Map.put("total", true) +      |> Map.put("limit", page_size) +      |> Map.put("offset", (page - 1) * page_size) + +    ActivityPub.fetch_activities([], params, :offset) +  end + +  @spec get_reports_grouped_by_status(%{required(:activity) => String.t()}) :: %{ +          required(:groups) => [ +            %{ +              required(:date) => String.t(), +              required(:account) => %{}, +              required(:status) => %{}, +              required(:actors) => [%User{}], +              required(:reports) => [%Activity{}] +            } +          ], +          required(:total) => integer +        } +  def get_reports_grouped_by_status(groups) do +    parsed_groups = +      groups +      |> Enum.map(fn entry -> +        activity = +          case Jason.decode(entry.activity) do +            {:ok, activity} -> activity +            _ -> build_flag_object(entry.activity) +          end + +        parse_report_group(activity) +      end) + +    %{ +      groups: parsed_groups +    } +  end + +  def parse_report_group(activity) do +    reports = get_reports_by_status_id(activity["id"]) +    max_date = Enum.max_by(reports, &NaiveDateTime.from_iso8601!(&1.data["published"])) +    actors = Enum.map(reports, & &1.user_actor) + +    %{ +      date: max_date.data["published"], +      account: activity["actor"], +      status: %{ +        id: activity["id"], +        content: activity["content"], +        published: activity["published"] +      }, +      actors: Enum.uniq(actors), +      reports: reports +    } +  end + +  def get_reports_by_status_id(ap_id) do +    from(a in Activity, +      where: fragment("(?)->>'type' = 'Flag'", a.data), +      where: fragment("(?)->'object' @> ?", a.data, ^[%{id: ap_id}]) +    ) +    |> Activity.with_preloaded_user_actor() +    |> Repo.all() +  end + +  @spec get_reported_activities() :: [ +          %{ +            required(:activity) => String.t(), +            required(:date) => String.t() +          } +        ] +  def get_reported_activities do +    from(a in Activity, +      where: fragment("(?)->>'type' = 'Flag'", a.data), +      select: %{ +        date: fragment("max(?->>'published') date", a.data), +        activity: +          fragment("jsonb_array_elements_text((? #- '{object,0}')->'object') activity", a.data) +      }, +      group_by: fragment("activity"), +      order_by: fragment("date DESC") +    ) +    |> Repo.all() +  end    def update_report_state(%Activity{} = activity, state)        when state in @strip_status_report_states do @@ -702,6 +887,18 @@ defmodule Pleroma.Web.ActivityPub.Utils do      |> Repo.update()    end +  def update_report_state(activity_ids, state) when state in @supported_report_states do +    activities_num = length(activity_ids) + +    from(a in Activity, where: a.id in ^activity_ids) +    |> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)]) +    |> Repo.update_all([]) +    |> case do +      {^activities_num, _} -> :ok +      _ -> {:error, activity_ids} +    end +  end +    def update_report_state(_, _), do: {:error, "Unsupported state"}    def strip_report_status_data(activity) do diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 30fc01755..8c1318d1b 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -11,6 +11,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do    alias Pleroma.UserInviteToken    alias Pleroma.Web.ActivityPub.ActivityPub    alias Pleroma.Web.ActivityPub.Relay +  alias Pleroma.Web.ActivityPub.Utils    alias Pleroma.Web.AdminAPI.AccountView    alias Pleroma.Web.AdminAPI.Config    alias Pleroma.Web.AdminAPI.ConfigView @@ -624,19 +625,17 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do    def list_reports(conn, params) do      {page, page_size} = page_params(params) -    params = -      params -      |> Map.put("type", "Flag") -      |> Map.put("skip_preload", true) -      |> Map.put("total", true) -      |> Map.put("limit", page_size) -      |> Map.put("offset", (page - 1) * page_size) +    conn +    |> put_view(ReportView) +    |> render("index.json", %{reports: Utils.get_reports(params, page, page_size)}) +  end -    reports = ActivityPub.fetch_activities([], params, :offset) +  def list_grouped_reports(conn, _params) do +    reports = Utils.get_reported_activities()      conn      |> put_view(ReportView) -    |> render("index.json", %{reports: reports}) +    |> render("index_grouped.json", Utils.get_reports_grouped_by_status(reports))    end    def report_show(conn, %{"id" => id}) do @@ -649,17 +648,26 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do      end    end -  def report_update_state(%{assigns: %{user: admin}} = conn, %{"id" => id, "state" => state}) do -    with {:ok, report} <- CommonAPI.update_report_state(id, state) do -      ModerationLog.insert_log(%{ -        action: "report_update", -        actor: admin, -        subject: report -      }) +  def reports_update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) do +    result = +      reports +      |> Enum.map(fn report -> +        with {:ok, activity} <- CommonAPI.update_report_state(report["id"], report["state"]) do +          ModerationLog.insert_log(%{ +            action: "report_update", +            actor: admin, +            subject: activity +          }) + +          activity +        else +          {:error, message} -> %{id: report["id"], error: message} +        end +      end) -      conn -      |> put_view(ReportView) -      |> render("show.json", Report.extract_report_info(report)) +    case Enum.any?(result, &Map.has_key?(&1, :error)) do +      true -> json_response(conn, :bad_request, result) +      false -> json_response(conn, :no_content, "")      end    end diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex index 101a74c63..ca88595c7 100644 --- a/lib/pleroma/web/admin_api/views/report_view.ex +++ b/lib/pleroma/web/admin_api/views/report_view.ex @@ -42,6 +42,26 @@ defmodule Pleroma.Web.AdminAPI.ReportView do      }    end +  def render("index_grouped.json", %{groups: groups}) do +    reports = +      Enum.map(groups, fn group -> +        %{ +          date: group[:date], +          account: group[:account], +          status: group[:status], +          actors: Enum.map(group[:actors], &merge_account_views/1), +          reports: +            group[:reports] +            |> Enum.map(&Report.extract_report_info(&1)) +            |> Enum.map(&render(__MODULE__, "show.json", &1)) +        } +      end) + +    %{ +      reports: reports +    } +  end +    defp merge_account_views(%User{} = user) do      Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user})      |> Map.merge(Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user})) diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index e57345621..fe6e26a90 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -120,6 +120,25 @@ defmodule Pleroma.Web.CommonAPI do      end    end +  def react_with_emoji(id, user, emoji) do +    with %Activity{} = activity <- Activity.get_by_id(id), +         object <- Object.normalize(activity) do +      ActivityPub.react_with_emoji(user, object, emoji) +    else +      _ -> +        {:error, dgettext("errors", "Could not add reaction emoji")} +    end +  end + +  def unreact_with_emoji(id, user, emoji) do +    with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji) do +      ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"]) +    else +      _ -> +        {:error, dgettext("errors", "Could not remove reaction emoji")} +    end +  end +    def vote(user, %{data: %{"type" => "Question"}} = object, choices) do      with :ok <- validate_not_author(object, user),           :ok <- validate_existing_votes(user, object), @@ -351,6 +370,13 @@ defmodule Pleroma.Web.CommonAPI do      end    end +  def update_report_state(activity_ids, state) when is_list(activity_ids) do +    case Utils.update_report_state(activity_ids, state) do +      :ok -> {:ok, activity_ids} +      _ -> {:error, dgettext("errors", "Could not update state")} +    end +  end +    def update_report_state(activity_id, state) do      with %Activity{} = activity <- Activity.get_by_id(activity_id) do        Utils.update_report_state(activity, state) diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 73fad519e..5b01b964b 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -66,9 +66,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do    @relations [:follow, :unfollow]    @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a -  plug(RateLimiter, {:relations_id_action, params: ["id", "uri"]} when action in @relations) -  plug(RateLimiter, :relations_actions when action in @relations) -  plug(RateLimiter, :app_account_creation when action == :create) +  plug(RateLimiter, [name: :relations_id_action, params: ["id", "uri"]] when action in @relations) +  plug(RateLimiter, [name: :relations_actions] when action in @relations) +  plug(RateLimiter, [name: :app_account_creation] when action == :create)    plug(:assign_account_by_id when action in @needs_account)    action_fallback(Pleroma.Web.MastodonAPI.FallbackController) diff --git a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex index bfd5120ba..d9e51de7f 100644 --- a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex @@ -15,7 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do    @local_mastodon_name "Mastodon-Local" -  plug(Pleroma.Plugs.RateLimiter, :password_reset when action == :password_reset) +  plug(Pleroma.Plugs.RateLimiter, [name: :password_reset] when action == :password_reset)    @doc "GET /web/login"    def login(%{assigns: %{user: %User{}}} = conn, _params) do diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex index 6cfd68a84..0a929f55b 100644 --- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex @@ -22,7 +22,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do    plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) -  plug(RateLimiter, :search when action in [:search, :search2, :account_search]) +  plug(RateLimiter, [name: :search] when action in [:search, :search2, :account_search])    def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do      accounts = User.search(query, search_options(params, user)) diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index e5d016f63..74b223cf4 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -82,17 +82,17 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do    plug(      RateLimiter, -    {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]} +    [name: :status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]]      when action in ~w(reblog unreblog)a    )    plug(      RateLimiter, -    {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]} +    [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]]      when action in ~w(favourite unfavourite)a    ) -  plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions) +  plug(RateLimiter, [name: :statuses_actions] when action in @rate_limited_status_actions)    action_fallback(Pleroma.Web.MastodonAPI.FallbackController) diff --git a/lib/pleroma/web/mastodon_api/views/conversation_view.ex b/lib/pleroma/web/mastodon_api/views/conversation_view.ex index c5998e661..2220fbcb1 100644 --- a/lib/pleroma/web/mastodon_api/views/conversation_view.ex +++ b/lib/pleroma/web/mastodon_api/views/conversation_view.ex @@ -12,7 +12,10 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do    alias Pleroma.Web.MastodonAPI.StatusView    def render("participations.json", %{participations: participations, for: user}) do -    render_many(participations, __MODULE__, "participation.json", as: :participation, for: user) +    safe_render_many(participations, __MODULE__, "participation.json", %{ +      as: :participation, +      for: user +    })    end    def render("participation.json", %{participation: participation, for: user}) do diff --git a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex b/lib/pleroma/web/mongooseim/mongoose_im_controller.ex index 6ed181cff..358600e7d 100644 --- a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex +++ b/lib/pleroma/web/mongooseim/mongoose_im_controller.ex @@ -10,8 +10,8 @@ defmodule Pleroma.Web.MongooseIM.MongooseIMController do    alias Pleroma.Repo    alias Pleroma.User -  plug(RateLimiter, :authentication when action in [:user_exists, :check_password]) -  plug(RateLimiter, {:authentication, params: ["user"]} when action == :check_password) +  plug(RateLimiter, [name: :authentication] when action in [:user_exists, :check_password]) +  plug(RateLimiter, [name: :authentication, params: ["user"]] when action == :check_password)    def user_exists(conn, %{"user" => username}) do      with %User{} <- Repo.get_by(User, nickname: username, local: true) do diff --git a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex index d7ae503f6..486b9f6a4 100644 --- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex +++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex @@ -46,6 +46,7 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do          data          |> Map.merge(%{quarantined_instances: quarantined}) +        |> Map.put(:enabled, Config.get([:instance, :federating]))        else          %{}        end diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index fe71aca8c..2aee8cab2 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do    use Pleroma.Web, :controller    alias Pleroma.Helpers.UriHelper +  alias Pleroma.Plugs.RateLimiter    alias Pleroma.Registration    alias Pleroma.Repo    alias Pleroma.User @@ -24,7 +25,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do    plug(:fetch_session)    plug(:fetch_flash) -  plug(Pleroma.Plugs.RateLimiter, :authentication when action == :create_authorization) +  plug(RateLimiter, [name: :authentication] when action == :create_authorization)    action_fallback(Pleroma.Web.OAuth.FallbackController) @@ -36,7 +37,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do      authorize(conn, Map.merge(params, auth_attrs))    end -  def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, params) do +  def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, %{"force_login" => _} = params) do      if ControllerHelper.truthy_param?(params["force_login"]) do        do_authorize(conn, params)      else @@ -44,6 +45,22 @@ defmodule Pleroma.Web.OAuth.OAuthController do      end    end +  # Note: the token is set in oauth_plug, but the token and client do not always go together. +  # For example, MastodonFE's token is set if user requests with another client, +  # after user already authorized to MastodonFE. +  # So we have to check client and token. +  def authorize( +        %Plug.Conn{assigns: %{token: %Token{} = token}} = conn, +        %{"client_id" => client_id} = params +      ) do +    with %Token{} = t <- Repo.get_by(Token, token: token.token) |> Repo.preload(:app), +         ^client_id <- t.app.client_id do +      handle_existing_authorization(conn, params) +    else +      _ -> do_authorize(conn, params) +    end +  end +    def authorize(%Plug.Conn{} = conn, params), do: do_authorize(conn, params)    defp do_authorize(%Plug.Conn{} = conn, params) do diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex index 6958519de..12a7c2365 100644 --- a/lib/pleroma/web/ostatus/ostatus_controller.ex +++ b/lib/pleroma/web/ostatus/ostatus_controller.ex @@ -8,6 +8,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do    alias Fallback.RedirectController    alias Pleroma.Activity    alias Pleroma.Object +  alias Pleroma.Plugs.RateLimiter    alias Pleroma.User    alias Pleroma.Web.ActivityPub.ActivityPubController    alias Pleroma.Web.ActivityPub.ObjectView @@ -17,8 +18,8 @@ defmodule Pleroma.Web.OStatus.OStatusController do    alias Pleroma.Web.Router    plug( -    Pleroma.Plugs.RateLimiter, -    {:ap_routes, params: ["uuid"]} when action in [:object, :activity] +    RateLimiter, +    [name: :ap_routes, params: ["uuid"]] when action in [:object, :activity]    )    plug( diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex index db6faac83..bc2f1017c 100644 --- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -42,7 +42,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do      when action != :confirmation_resend    ) -  plug(RateLimiter, :account_confirmation_resend when action == :confirmation_resend) +  plug(RateLimiter, [name: :account_confirmation_resend] when action == :confirmation_resend)    plug(:assign_account_by_id when action in [:favourites, :subscribe, :unsubscribe])    plug(:put_view, Pleroma.Web.MastodonAPI.AccountView) diff --git a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex index 651a99423..8fed3f5bb 100644 --- a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex @@ -7,10 +7,15 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do    import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] +  alias Pleroma.Activity    alias Pleroma.Conversation.Participation    alias Pleroma.Notification +  alias Pleroma.Object    alias Pleroma.Plugs.OAuthScopesPlug +  alias Pleroma.User    alias Pleroma.Web.ActivityPub.ActivityPub +  alias Pleroma.Web.CommonAPI +  alias Pleroma.Web.MastodonAPI.AccountView    alias Pleroma.Web.MastodonAPI.ConversationView    alias Pleroma.Web.MastodonAPI.NotificationView    alias Pleroma.Web.MastodonAPI.StatusView @@ -29,6 +34,47 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do    plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) +  def emoji_reactions_by(%{assigns: %{user: user}} = conn, %{"id" => activity_id}) do +    with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id), +         %Object{data: %{"reactions" => emoji_reactions}} <- Object.normalize(activity) do +      reactions = +        emoji_reactions +        |> Enum.map(fn {emoji, users} -> +          users = Enum.map(users, &User.get_cached_by_ap_id/1) +          {emoji, AccountView.render("index.json", %{users: users, for: user, as: :user})} +        end) +        |> Enum.into(%{}) + +      conn +      |> json(reactions) +    else +      _e -> +        conn +        |> json(%{}) +    end +  end + +  def react_with_emoji(%{assigns: %{user: user}} = conn, %{"id" => activity_id, "emoji" => emoji}) do +    with {:ok, _activity, _object} <- CommonAPI.react_with_emoji(activity_id, user, emoji), +         activity <- Activity.get_by_id(activity_id) do +      conn +      |> put_view(StatusView) +      |> render("show.json", %{activity: activity, for: user, as: :activity}) +    end +  end + +  def unreact_with_emoji(%{assigns: %{user: user}} = conn, %{ +        "id" => activity_id, +        "emoji" => emoji +      }) do +    with {:ok, _activity, _object} <- CommonAPI.unreact_with_emoji(activity_id, user, emoji), +         activity <- Activity.get_by_id(activity_id) do +      conn +      |> put_view(StatusView) +      |> render("show.json", %{activity: activity, for: user, as: :activity}) +    end +  end +    def conversation(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do      with %Participation{} = participation <- Participation.get(participation_id),           true <- user.id == participation.user_id do diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index af7c0e289..129da422c 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -179,8 +179,9 @@ defmodule Pleroma.Web.Router do      get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses)      get("/reports", AdminAPIController, :list_reports) +    get("/grouped_reports", AdminAPIController, :list_grouped_reports)      get("/reports/:id", AdminAPIController, :report_show) -    put("/reports/:id", AdminAPIController, :report_update_state) +    patch("/reports", AdminAPIController, :reports_update)      post("/reports/:id/respond", AdminAPIController, :report_respond)      put("/statuses/:id", AdminAPIController, :status_update) @@ -262,6 +263,12 @@ defmodule Pleroma.Web.Router do    end    scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do +    pipe_through(:api) + +    get("/statuses/:id/emoji_reactions_by", PleromaAPIController, :emoji_reactions_by) +  end + +  scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do      scope [] do        pipe_through(:authenticated_api) @@ -274,6 +281,8 @@ defmodule Pleroma.Web.Router do        pipe_through(:authenticated_api)        patch("/conversations/:id", PleromaAPIController, :update_conversation) +      post("/statuses/:id/react_with_emoji", PleromaAPIController, :react_with_emoji) +      post("/statuses/:id/unreact_with_emoji", PleromaAPIController, :unreact_with_emoji)        post("/notifications/read", PleromaAPIController, :read_notification)        patch("/accounts/update_avatar", AccountController, :update_avatar) @@ -496,6 +505,7 @@ defmodule Pleroma.Web.Router do    pipeline :ostatus do      plug(:accepts, ["html", "xml", "atom", "activity+json", "json"]) +    plug(Pleroma.Plugs.StaticFEPlug)    end    pipeline :oembed do diff --git a/lib/pleroma/web/static_fe/static_fe_controller.ex b/lib/pleroma/web/static_fe/static_fe_controller.ex new file mode 100644 index 000000000..8ccf15f4b --- /dev/null +++ b/lib/pleroma/web/static_fe/static_fe_controller.ex @@ -0,0 +1,163 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.StaticFE.StaticFEController do +  use Pleroma.Web, :controller + +  alias Pleroma.Activity +  alias Pleroma.Object +  alias Pleroma.User +  alias Pleroma.Web.ActivityPub.ActivityPub +  alias Pleroma.Web.ActivityPub.Visibility +  alias Pleroma.Web.Metadata +  alias Pleroma.Web.Router.Helpers + +  plug(:put_layout, :static_fe) +  plug(:put_view, Pleroma.Web.StaticFE.StaticFEView) +  plug(:assign_id) + +  @page_keys ["max_id", "min_id", "limit", "since_id", "order"] + +  defp get_title(%Object{data: %{"name" => name}}) when is_binary(name), +    do: name + +  defp get_title(%Object{data: %{"summary" => summary}}) when is_binary(summary), +    do: summary + +  defp get_title(_), do: nil + +  defp not_found(conn, message) do +    conn +    |> put_status(404) +    |> render("error.html", %{message: message, meta: ""}) +  end + +  def get_counts(%Activity{} = activity) do +    %Object{data: data} = Object.normalize(activity) + +    %{ +      likes: data["like_count"] || 0, +      replies: data["repliesCount"] || 0, +      announces: data["announcement_count"] || 0 +    } +  end + +  def represent(%Activity{} = activity), do: represent(activity, false) + +  def represent(%Activity{object: %Object{data: data}} = activity, selected) do +    {:ok, user} = User.get_or_fetch(activity.object.data["actor"]) + +    link = +      case user.local do +        true -> Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity) +        _ -> data["url"] || data["external_url"] || data["id"] +      end + +    %{ +      user: user, +      title: get_title(activity.object), +      content: data["content"] || nil, +      attachment: data["attachment"], +      link: link, +      published: data["published"], +      sensitive: data["sensitive"], +      selected: selected, +      counts: get_counts(activity), +      id: activity.id +    } +  end + +  def show(%{assigns: %{notice_id: notice_id}} = conn, _params) do +    with %Activity{local: true} = activity <- +           Activity.get_by_id_with_object(notice_id), +         true <- Visibility.is_public?(activity.object), +         %User{} = user <- User.get_by_ap_id(activity.object.data["actor"]) do +      meta = Metadata.build_tags(%{activity_id: notice_id, object: activity.object, user: user}) + +      timeline = +        activity.object.data["context"] +        |> ActivityPub.fetch_activities_for_context(%{}) +        |> Enum.reverse() +        |> Enum.map(&represent(&1, &1.object.id == activity.object.id)) + +      render(conn, "conversation.html", %{activities: timeline, meta: meta}) +    else +      %Activity{object: %Object{data: data}} -> +        conn +        |> put_status(:found) +        |> redirect(external: data["url"] || data["external_url"] || data["id"]) + +      _ -> +        not_found(conn, "Post not found.") +    end +  end + +  def show(%{assigns: %{username_or_id: username_or_id}} = conn, params) do +    case User.get_cached_by_nickname_or_id(username_or_id) do +      %User{} = user -> +        meta = Metadata.build_tags(%{user: user}) + +        timeline = +          ActivityPub.fetch_user_activities(user, nil, Map.take(params, @page_keys)) +          |> Enum.map(&represent/1) + +        prev_page_id = +          (params["min_id"] || params["max_id"]) && +            List.first(timeline) && List.first(timeline).id + +        next_page_id = List.last(timeline) && List.last(timeline).id + +        render(conn, "profile.html", %{ +          user: user, +          timeline: timeline, +          prev_page_id: prev_page_id, +          next_page_id: next_page_id, +          meta: meta +        }) + +      _ -> +        not_found(conn, "User not found.") +    end +  end + +  def show(%{assigns: %{object_id: _}} = conn, _params) do +    url = Helpers.url(conn) <> conn.request_path + +    case Activity.get_create_by_object_ap_id_with_object(url) do +      %Activity{} = activity -> +        to = Helpers.o_status_path(Pleroma.Web.Endpoint, :notice, activity) +        redirect(conn, to: to) + +      _ -> +        not_found(conn, "Post not found.") +    end +  end + +  def show(%{assigns: %{activity_id: _}} = conn, _params) do +    url = Helpers.url(conn) <> conn.request_path + +    case Activity.get_by_ap_id(url) do +      %Activity{} = activity -> +        to = Helpers.o_status_path(Pleroma.Web.Endpoint, :notice, activity) +        redirect(conn, to: to) + +      _ -> +        not_found(conn, "Post not found.") +    end +  end + +  def assign_id(%{path_info: ["notice", notice_id]} = conn, _opts), +    do: assign(conn, :notice_id, notice_id) + +  def assign_id(%{path_info: ["users", user_id]} = conn, _opts), +    do: assign(conn, :username_or_id, user_id) + +  def assign_id(%{path_info: ["objects", object_id]} = conn, _opts), +    do: assign(conn, :object_id, object_id) + +  def assign_id(%{path_info: ["activities", activity_id]} = conn, _opts), +    do: assign(conn, :activity_id, activity_id) + +  def assign_id(conn, _opts), do: conn +end diff --git a/lib/pleroma/web/static_fe/static_fe_view.ex b/lib/pleroma/web/static_fe/static_fe_view.ex new file mode 100644 index 000000000..821ece9a9 --- /dev/null +++ b/lib/pleroma/web/static_fe/static_fe_view.ex @@ -0,0 +1,47 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.StaticFE.StaticFEView do +  use Pleroma.Web, :view + +  alias Calendar.Strftime +  alias Pleroma.Emoji.Formatter +  alias Pleroma.User +  alias Pleroma.Web.Endpoint +  alias Pleroma.Web.Gettext +  alias Pleroma.Web.MediaProxy +  alias Pleroma.Web.Metadata.Utils +  alias Pleroma.Web.Router.Helpers + +  use Phoenix.HTML + +  @media_types ["image", "audio", "video"] + +  def emoji_for_user(%User{} = user) do +    user.source_data +    |> Map.get("tag", []) +    |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end) +    |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} -> +      {String.trim(name, ":"), url} +    end) +  end + +  def fetch_media_type(%{"mediaType" => mediaType}) do +    Utils.fetch_media_type(@media_types, mediaType) +  end + +  def format_date(date) do +    {:ok, date, _} = DateTime.from_iso8601(date) +    Strftime.strftime!(date, "%Y/%m/%d %l:%M:%S %p UTC") +  end + +  def instance_name, do: Pleroma.Config.get([:instance, :name], "Pleroma") + +  def open_content? do +    Pleroma.Config.get( +      [:frontend_configurations, :collapse_message_with_subjects], +      true +    ) +  end +end diff --git a/lib/pleroma/web/templates/layout/static_fe.html.eex b/lib/pleroma/web/templates/layout/static_fe.html.eex new file mode 100644 index 000000000..819632cec --- /dev/null +++ b/lib/pleroma/web/templates/layout/static_fe.html.eex @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html> +  <head> +    <meta charset="utf-8" /> +    <meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui" /> +    <title><%= Pleroma.Config.get([:instance, :name]) %></title> +    <%= Phoenix.HTML.raw(assigns[:meta] || "") %> +    <link rel="stylesheet" href="/static/static-fe.css"> +  </head> +  <body> +    <div class="container"> +      <%= render @view_module, @view_template, assigns %> +    </div> +  </body> +</html> diff --git a/lib/pleroma/web/templates/static_fe/static_fe/_attachment.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/_attachment.html.eex new file mode 100644 index 000000000..7e04e9550 --- /dev/null +++ b/lib/pleroma/web/templates/static_fe/static_fe/_attachment.html.eex @@ -0,0 +1,8 @@ +<%= case @mediaType do %> +<% "audio" -> %> +<audio src="<%= @url %>" controls="controls"></audio> +<% "video" -> %> +<video src="<%= @url %>" controls="controls"></video> +<% _ -> %> +<img src="<%= @url %>" alt="<%= @name %>" title="<%= @name %>"> +<% end %> diff --git a/lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex new file mode 100644 index 000000000..df5e5eedd --- /dev/null +++ b/lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex @@ -0,0 +1,37 @@ +<div class="activity" <%= if @selected do %> id="selected" <% end %>> +  <p class="pull-right"> +    <%= link format_date(@published), to: @link, class: "activity-link" %> +  </p> +  <%= render("_user_card.html", %{user: @user}) %> +  <div class="activity-content"> +    <%= if @title != "" do %> +      <details <%= if open_content?() do %>open<% end %>> +        <summary><%= raw @title %></summary> +        <div class="e-content"><%= raw @content %></div> +      </details> +    <% else %> +      <div class="e-content"><%= raw @content %></div> +    <% end %> +    <%= for %{"name" => name, "url" => [url | _]} <- @attachment do %> +      <%= if @sensitive do %> +        <details class="nsfw"> +          <summary><%= Gettext.gettext("sensitive media") %></summary> +          <div> +            <%= render("_attachment.html", %{name: name, url: url["href"], +                                             mediaType: fetch_media_type(url)}) %> +          </div> +        </details> +      <% else %> +        <%= render("_attachment.html", %{name: name, url: url["href"], +                                         mediaType: fetch_media_type(url)}) %> +      <% end %> +    <% end %> +  </div> +  <%= if @selected do %> +    <dl class="counts"> +      <dt><%= Gettext.gettext("replies") %></dt><dd><%= @counts.replies %></dd> +      <dt><%= Gettext.gettext("announces") %></dt><dd><%= @counts.announces %></dd> +      <dt><%= Gettext.gettext("likes") %></dt><dd><%= @counts.likes %></dd> +    </dl> +  <% end %> +</div> diff --git a/lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex new file mode 100644 index 000000000..c7789f9ac --- /dev/null +++ b/lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex @@ -0,0 +1,11 @@ +<div class="p-author h-card"> +  <a class="u-url" rel="author noopener" href="<%= User.profile_url(@user) %>"> +    <div class="avatar"> +      <img src="<%= User.avatar_url(@user) |> MediaProxy.url %>" width="48" height="48" alt=""> +    </div> +    <span class="display-name"> +      <bdi><%= raw (@user.name |> Formatter.emojify(emoji_for_user(@user))) %></bdi> +      <span class="nickname"><%= @user.nickname %></span> +    </span> +  </a> +</div> diff --git a/lib/pleroma/web/templates/static_fe/static_fe/conversation.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/conversation.html.eex new file mode 100644 index 000000000..2acd84828 --- /dev/null +++ b/lib/pleroma/web/templates/static_fe/static_fe/conversation.html.eex @@ -0,0 +1,11 @@ +<header> +  <h1><%= link instance_name(), to: "/" %></h1> +</header> + +<main> +  <div class="conversation"> +    <%= for activity <- @activities do %> +      <%= render("_notice.html", activity) %> +    <% end %> +  </div> +</main> diff --git a/lib/pleroma/web/templates/static_fe/static_fe/error.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/error.html.eex new file mode 100644 index 000000000..d98a1eba7 --- /dev/null +++ b/lib/pleroma/web/templates/static_fe/static_fe/error.html.eex @@ -0,0 +1,7 @@ +<header> +  <h1><%= gettext("Oops") %></h1> +</header> + +<main> +  <p><%= @message %></p> +</main> diff --git a/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex new file mode 100644 index 000000000..94063c92d --- /dev/null +++ b/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex @@ -0,0 +1,31 @@ +<header> +  <h1><%= link instance_name(), to: "/" %></h1> + +  <h3> +    <form class="pull-right collapse" method="POST" action="<%= Helpers.util_path(@conn, :remote_subscribe) %>"> +      <input type="hidden" name="nickname" value="<%= @user.nickname %>"> +      <input type="hidden" name="profile" value=""> +      <button type="submit" class="collapse">Remote follow</button> +    </form> +    <%= raw Formatter.emojify(@user.name, emoji_for_user(@user)) %> | +    <%= link "@#{@user.nickname}@#{Endpoint.host()}", to: User.profile_url(@user) %> +  </h3> +  <p><%= raw @user.bio %></p> +</header> + +<main> +  <div class="activity-stream"> +    <%= for activity <- @timeline do %> +      <%= render("_notice.html", Map.put(activity, :selected, false)) %> +    <% end %> +    <p id="pagination"> +      <%= if @prev_page_id do %> +        <%= link "«", to: "?min_id=" <> @prev_page_id %> +      <% end %> +      <%= if @prev_page_id && @next_page_id, do: " | " %> +      <%= if @next_page_id do %> +        <%= link "»", to: "?max_id=" <> @next_page_id %> +      <% end %> +    </p> +  </div> +</main> | 
