Merge branch 'documentation-migration' of akkoma.dev:AkkomaGang/akkoma into documentation-migration
This commit is contained in:
		
						commit
						ff89d4526a
					
				
					 117 changed files with 5049 additions and 1331 deletions
				
			
		| 
						 | 
				
			
			@ -16,7 +16,9 @@ pipeline:
 | 
			
		|||
  glibc:
 | 
			
		||||
    when:
 | 
			
		||||
      event:
 | 
			
		||||
        - tag
 | 
			
		||||
        - push
 | 
			
		||||
      branch:
 | 
			
		||||
        - develop
 | 
			
		||||
    secrets:
 | 
			
		||||
    - SCW_ACCESS_KEY
 | 
			
		||||
    - SCW_SECRET_KEY
 | 
			
		||||
| 
						 | 
				
			
			@ -44,7 +46,9 @@ pipeline:
 | 
			
		|||
  musl:
 | 
			
		||||
    when:
 | 
			
		||||
      event:
 | 
			
		||||
        - tag
 | 
			
		||||
        - push
 | 
			
		||||
      branch:
 | 
			
		||||
        - develop
 | 
			
		||||
    secrets:
 | 
			
		||||
    - SCW_ACCESS_KEY
 | 
			
		||||
    - SCW_SECRET_KEY
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,6 +11,7 @@ pipeline:
 | 
			
		|||
    when:
 | 
			
		||||
      event:
 | 
			
		||||
      - push
 | 
			
		||||
      - pull_request
 | 
			
		||||
    environment:
 | 
			
		||||
      MIX_ENV: test
 | 
			
		||||
    commands:
 | 
			
		||||
| 
						 | 
				
			
			@ -25,6 +26,7 @@ pipeline:
 | 
			
		|||
    when:
 | 
			
		||||
      event:
 | 
			
		||||
      - push
 | 
			
		||||
      - pull_request
 | 
			
		||||
    environment:
 | 
			
		||||
      MIX_ENV: test
 | 
			
		||||
      POSTGRES_DB: pleroma_test
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -54,6 +54,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 | 
			
		|||
- Readded mastoFE
 | 
			
		||||
- Added support for custom emoji reactions
 | 
			
		||||
- Added `emoji_url` in notifications to allow for custom emoji rendering
 | 
			
		||||
- Make backend-rendered pages translatable. This includes emails. Pages returned as a HTTP response are translated using the language specified in the `userLanguage` cookie, or the `Accept-Language` header. Emails are translated using the `language` field when registering. This language can be changed by `PATCH /api/v1/accounts/update_credentials` with the `language` field.
 | 
			
		||||
 | 
			
		||||
### Fixed
 | 
			
		||||
- Subscription(Bell) Notifications: Don't create from Pipeline Ingested replies
 | 
			
		||||
| 
						 | 
				
			
			@ -116,6 +117,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 | 
			
		|||
- Improved Twittercard and OpenGraph meta tag generation including thumbnails and image dimension metadata when available.
 | 
			
		||||
- AdminAPI: sort users so the newest are at the top.
 | 
			
		||||
- ActivityPub Client-to-Server(C2S): Limitation on the type of Activity/Object are lifted as they are now passed through ObjectValidators
 | 
			
		||||
- MRF (`AntiFollowbotPolicy`): Bot accounts are now also considered followbots. Users can still allow bots to follow them by first following the bot.
 | 
			
		||||
 | 
			
		||||
### Added
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -568,7 +568,8 @@
 | 
			
		|||
    remote_fetcher: 2,
 | 
			
		||||
    attachments_cleanup: 1,
 | 
			
		||||
    new_users_digest: 1,
 | 
			
		||||
    mute_expire: 5
 | 
			
		||||
    mute_expire: 5,
 | 
			
		||||
    search_indexing: 10
 | 
			
		||||
  ],
 | 
			
		||||
  plugins: [Oban.Plugins.Pruner],
 | 
			
		||||
  crontab: [
 | 
			
		||||
| 
						 | 
				
			
			@ -579,7 +580,8 @@
 | 
			
		|||
config :pleroma, :workers,
 | 
			
		||||
  retries: [
 | 
			
		||||
    federator_incoming: 5,
 | 
			
		||||
    federator_outgoing: 5
 | 
			
		||||
    federator_outgoing: 5,
 | 
			
		||||
    search_indexing: 2
 | 
			
		||||
  ]
 | 
			
		||||
 | 
			
		||||
config :pleroma, Pleroma.Formatter,
 | 
			
		||||
| 
						 | 
				
			
			@ -842,17 +844,32 @@
 | 
			
		|||
 | 
			
		||||
config :pleroma, ConcurrentLimiter, [
 | 
			
		||||
  {Pleroma.Web.RichMedia.Helpers, [max_running: 5, max_waiting: 5]},
 | 
			
		||||
  {Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy, [max_running: 5, max_waiting: 5]}
 | 
			
		||||
  {Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy, [max_running: 5, max_waiting: 5]},
 | 
			
		||||
  {Pleroma.Search, [max_running: 30, max_waiting: 50]}
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
config :pleroma, :search, provider: Pleroma.Search.Builtin
 | 
			
		||||
config :pleroma, Pleroma.Search, module: Pleroma.Search.DatabaseSearch
 | 
			
		||||
 | 
			
		||||
config :pleroma, :telemetry,
 | 
			
		||||
  slow_queries_logging: [
 | 
			
		||||
    enabled: false,
 | 
			
		||||
    min_duration: 500_000,
 | 
			
		||||
    exclude_sources: [nil, "oban_jobs"]
 | 
			
		||||
  ]
 | 
			
		||||
config :pleroma, Pleroma.Search.Meilisearch,
 | 
			
		||||
  url: "http://127.0.0.1:7700/",
 | 
			
		||||
  private_key: nil,
 | 
			
		||||
  initial_indexing_chunk_size: 100_000
 | 
			
		||||
 | 
			
		||||
config :pleroma, Pleroma.Search.Elasticsearch.Cluster,
 | 
			
		||||
  url: "http://localhost:9200",
 | 
			
		||||
  username: "elastic",
 | 
			
		||||
  password: "changeme",
 | 
			
		||||
  api: Elasticsearch.API.HTTP,
 | 
			
		||||
  json_library: Jason,
 | 
			
		||||
  indexes: %{
 | 
			
		||||
    activities: %{
 | 
			
		||||
      settings: "priv/es-mappings/activity.json",
 | 
			
		||||
      store: Pleroma.Search.Elasticsearch.Store,
 | 
			
		||||
      sources: [Pleroma.Activity],
 | 
			
		||||
      bulk_page_size: 1000,
 | 
			
		||||
      bulk_wait_interval: 15_000
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
# Import environment specific config. This must remain at the bottom
 | 
			
		||||
# of this file so it overrides the configuration defined above.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3358,5 +3358,133 @@
 | 
			
		|||
        ]
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  %{
 | 
			
		||||
    group: :pleroma,
 | 
			
		||||
    key: Pleroma.Search,
 | 
			
		||||
    type: :group,
 | 
			
		||||
    description: "General search settings.",
 | 
			
		||||
    children: [
 | 
			
		||||
      %{
 | 
			
		||||
        key: :module,
 | 
			
		||||
        type: :keyword,
 | 
			
		||||
        description: "Selected search module.",
 | 
			
		||||
        suggestion: [Pleroma.Search.DatabaseSearch, Pleroma.Search.Meilisearch]
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  %{
 | 
			
		||||
    group: :pleroma,
 | 
			
		||||
    key: Pleroma.Search.Meilisearch,
 | 
			
		||||
    type: :group,
 | 
			
		||||
    description: "Meilisearch settings.",
 | 
			
		||||
    children: [
 | 
			
		||||
      %{
 | 
			
		||||
        key: :url,
 | 
			
		||||
        type: :string,
 | 
			
		||||
        description: "Meilisearch URL.",
 | 
			
		||||
        suggestion: ["http://127.0.0.1:7700/"]
 | 
			
		||||
      },
 | 
			
		||||
      %{
 | 
			
		||||
        key: :private_key,
 | 
			
		||||
        type: :string,
 | 
			
		||||
        description:
 | 
			
		||||
          "Private key for meilisearch authentication, or `nil` to disable private key authentication.",
 | 
			
		||||
        suggestion: [nil]
 | 
			
		||||
      },
 | 
			
		||||
      %{
 | 
			
		||||
        key: :initial_indexing_chunk_size,
 | 
			
		||||
        type: :int,
 | 
			
		||||
        description:
 | 
			
		||||
          "Amount of posts in a batch when running the initial indexing operation. Should probably not be more than 100000" <>
 | 
			
		||||
            " since there's a limit on maximum insert size",
 | 
			
		||||
        suggestion: [100_000]
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  %{
 | 
			
		||||
    group: :pleroma,
 | 
			
		||||
    key: Pleroma.Search.Elasticsearch.Cluster,
 | 
			
		||||
    type: :group,
 | 
			
		||||
    description: "Elasticsearch settings.",
 | 
			
		||||
    children: [
 | 
			
		||||
      %{
 | 
			
		||||
        key: :url,
 | 
			
		||||
        type: :string,
 | 
			
		||||
        description: "Elasticsearch URL.",
 | 
			
		||||
        suggestion: ["http://127.0.0.1:9200/"]
 | 
			
		||||
      },
 | 
			
		||||
      %{
 | 
			
		||||
        key: :username,
 | 
			
		||||
        type: :string,
 | 
			
		||||
        description: "Username to connect to ES. Set to nil if your cluster is unauthenticated.",
 | 
			
		||||
        suggestion: ["elastic"]
 | 
			
		||||
      },
 | 
			
		||||
      %{
 | 
			
		||||
        key: :password,
 | 
			
		||||
        type: :string,
 | 
			
		||||
        description: "Password to connect to ES. Set to nil if your cluster is unauthenticated.",
 | 
			
		||||
        suggestion: ["changeme"]
 | 
			
		||||
      },
 | 
			
		||||
      %{
 | 
			
		||||
        key: :api,
 | 
			
		||||
        type: :module,
 | 
			
		||||
        description:
 | 
			
		||||
          "The API module used by Elasticsearch. Should always be Elasticsearch.API.HTTP",
 | 
			
		||||
        suggestion: [Elasticsearch.API.HTTP]
 | 
			
		||||
      },
 | 
			
		||||
      %{
 | 
			
		||||
        key: :json_library,
 | 
			
		||||
        type: :module,
 | 
			
		||||
        description:
 | 
			
		||||
          "The JSON module used to encode/decode when communicating with Elasticsearch",
 | 
			
		||||
        suggestion: [Jason]
 | 
			
		||||
      },
 | 
			
		||||
      %{
 | 
			
		||||
        key: :indexes,
 | 
			
		||||
        type: :map,
 | 
			
		||||
        description: "The indices to set up in Elasticsearch",
 | 
			
		||||
        children: [
 | 
			
		||||
          %{
 | 
			
		||||
            key: :activities,
 | 
			
		||||
            type: :map,
 | 
			
		||||
            description: "Config for the index to use for activities",
 | 
			
		||||
            children: [
 | 
			
		||||
              %{
 | 
			
		||||
                key: :settings,
 | 
			
		||||
                type: :string,
 | 
			
		||||
                description:
 | 
			
		||||
                  "Path to the file containing index settings for the activities index. Should contain a mapping.",
 | 
			
		||||
                suggestion: ["priv/es-mappings/activity.json"]
 | 
			
		||||
              },
 | 
			
		||||
              %{
 | 
			
		||||
                key: :store,
 | 
			
		||||
                type: :module,
 | 
			
		||||
                description: "The internal store module",
 | 
			
		||||
                suggestion: [Pleroma.Search.Elasticsearch.Store]
 | 
			
		||||
              },
 | 
			
		||||
              %{
 | 
			
		||||
                key: :sources,
 | 
			
		||||
                type: {:list, :module},
 | 
			
		||||
                description: "The internal types to use for this index",
 | 
			
		||||
                suggestion: [[Pleroma.Activity]]
 | 
			
		||||
              },
 | 
			
		||||
              %{
 | 
			
		||||
                key: :bulk_page_size,
 | 
			
		||||
                type: :int,
 | 
			
		||||
                description: "Size for bulk put requests, mostly used on building the index",
 | 
			
		||||
                suggestion: [5000]
 | 
			
		||||
              },
 | 
			
		||||
              %{
 | 
			
		||||
                key: :bulk_wait_interval,
 | 
			
		||||
                type: :int,
 | 
			
		||||
                description: "Time to wait between bulk put requests (in ms)",
 | 
			
		||||
                suggestion: [15_000]
 | 
			
		||||
              }
 | 
			
		||||
            ]
 | 
			
		||||
          }
 | 
			
		||||
        ]
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -134,6 +134,10 @@
 | 
			
		|||
  ap_streamer: Pleroma.Web.ActivityPub.ActivityPubMock,
 | 
			
		||||
  logger: Pleroma.LoggerMock
 | 
			
		||||
 | 
			
		||||
config :pleroma, Pleroma.Search, module: Pleroma.Search.DatabaseSearch
 | 
			
		||||
 | 
			
		||||
config :pleroma, Pleroma.Search.Meilisearch, url: "http://127.0.0.1:7700/", private_key: nil
 | 
			
		||||
 | 
			
		||||
# Reduce recompilation time
 | 
			
		||||
# https://dashbit.co/blog/speeding-up-re-compilation-of-elixir-projects
 | 
			
		||||
config :phoenix, :plug_init_mode, :runtime
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,11 +17,11 @@ su pleroma -s $SHELL -lc "./bin/pleroma_ctl migrate"
 | 
			
		|||
## For from source installations (using git)
 | 
			
		||||
 | 
			
		||||
1. Go to the working directory of Pleroma (default is `/opt/pleroma`)
 | 
			
		||||
2. Run `git pull`. This pulls the latest changes from upstream.
 | 
			
		||||
2. Run `git pull` [^1]. This pulls the latest changes from upstream.
 | 
			
		||||
3. Run `mix deps.get` [^1]. This pulls in any new dependencies.
 | 
			
		||||
4. Stop the Pleroma service.
 | 
			
		||||
5. Run `mix ecto.migrate` [^1] [^2]. This task performs database migrations, if there were any.
 | 
			
		||||
6. Start the Pleroma service.
 | 
			
		||||
 | 
			
		||||
[^1]: Depending on which install guide you followed (for example on Debian/Ubuntu), you want to run `mix` tasks as `pleroma` user by adding `sudo -Hu pleroma` before the command.
 | 
			
		||||
[^1]: Depending on which install guide you followed (for example on Debian/Ubuntu), you want to run `git` and `mix` tasks as `pleroma` user by adding `sudo -Hu pleroma` before the command.
 | 
			
		||||
[^2]: Prefix with `MIX_ENV=prod` to run it using the production config file.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -125,6 +125,8 @@ To add configuration to your config file, you can copy it from the base config.
 | 
			
		|||
    * `Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy`: Sets a default expiration on all posts made by users of the local instance. Requires `Pleroma.Workers.PurgeExpiredActivity` to be enabled for processing the scheduled delections.
 | 
			
		||||
    * `Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy`: Makes all bot posts to disappear from public timelines.
 | 
			
		||||
    * `Pleroma.Web.ActivityPub.MRF.FollowBotPolicy`: Automatically follows newly discovered users from the specified bot account. Local accounts, locked accounts, and users with "#nobot" in their bio are respected and excluded from being followed.
 | 
			
		||||
    * `Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy`: Drops follow requests from followbots. Users can still allow bots to follow them by first following the bot.
 | 
			
		||||
    * `Pleroma.Web.ActivityPub.MRF.KeywordPolicy`: Rejects or removes from the federated timeline or replaces keywords. (See [`:mrf_keyword`](#mrf_keyword)).
 | 
			
		||||
* `transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo).
 | 
			
		||||
* `transparency_exclusions`: Exclude specific instance names from MRF transparency.  The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										163
									
								
								docs/configuration/search.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								docs/configuration/search.md
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,163 @@
 | 
			
		|||
# Configuring search
 | 
			
		||||
 | 
			
		||||
{! backend/administration/CLI_tasks/general_cli_task_info.include !}
 | 
			
		||||
 | 
			
		||||
## Built-in search
 | 
			
		||||
 | 
			
		||||
To use built-in search that has no external dependencies, set the search module to `Pleroma.Activity`:
 | 
			
		||||
 | 
			
		||||
> config :pleroma, Pleroma.Search, module: Pleroma.Search.DatabaseSearch
 | 
			
		||||
 | 
			
		||||
While it has no external dependencies, it has problems with performance and relevancy.
 | 
			
		||||
 | 
			
		||||
## Meilisearch
 | 
			
		||||
 | 
			
		||||
Note that it's quite a bit more memory hungry than PostgreSQL (around 4-5G for ~1.2 million
 | 
			
		||||
posts while idle and up to 7G while indexing initially). The disk usage for this additional index is also
 | 
			
		||||
around 4 gigabytes. Like [RUM](./cheatsheet.md#rum-indexing-for-full-text-search) indexes, it offers considerably
 | 
			
		||||
higher performance and ordering by timestamp in a reasonable amount of time.
 | 
			
		||||
Additionally, the search results seem to be more accurate.
 | 
			
		||||
 | 
			
		||||
Due to high memory usage, it may be best to set it up on a different machine, if running pleroma on a low-resource
 | 
			
		||||
computer, and use private key authentication to secure the remote search instance.
 | 
			
		||||
 | 
			
		||||
To use [meilisearch](https://www.meilisearch.com/), set the search module to `Pleroma.Search.Meilisearch`:
 | 
			
		||||
 | 
			
		||||
> config :pleroma, Pleroma.Search, module: Pleroma.Search.Meilisearch
 | 
			
		||||
 | 
			
		||||
You then need to set the address of the meilisearch instance, and optionally the private key for authentication. You might
 | 
			
		||||
also want to change the `initial_indexing_chunk_size` to be smaller if you're server is not very powerful, but not higher than `100_000`,
 | 
			
		||||
because meilisearch will refuse to process it if it's too big. However, in general you want this to be as big as possible, because meilisearch
 | 
			
		||||
indexes faster when it can process many posts in a single batch.
 | 
			
		||||
 | 
			
		||||
> config :pleroma, Pleroma.Search.Meilisearch,
 | 
			
		||||
>    url: "http://127.0.0.1:7700/",
 | 
			
		||||
>    private_key: "private key",
 | 
			
		||||
>    initial_indexing_chunk_size: 100_000
 | 
			
		||||
 | 
			
		||||
Information about setting up meilisearch can be found in the
 | 
			
		||||
[official documentation](https://docs.meilisearch.com/learn/getting_started/installation.html).
 | 
			
		||||
You probably want to start it with `MEILI_NO_ANALYTICS=true` environment variable to disable analytics.
 | 
			
		||||
At least version 0.25.0 is required, but you are strongly adviced to use at least 0.26.0, as it introduces
 | 
			
		||||
the `--enable-auto-batching` option which drastically improves performance. Without this option, the search
 | 
			
		||||
is hardly usable on a somewhat big instance.
 | 
			
		||||
 | 
			
		||||
### Private key authentication (optional)
 | 
			
		||||
 | 
			
		||||
To set the private key, use the `MEILI_MASTER_KEY` environment variable when starting. After setting the _master key_,
 | 
			
		||||
you have to get the _private key_, which is actually used for authentication.
 | 
			
		||||
 | 
			
		||||
=== "OTP"
 | 
			
		||||
    ```sh
 | 
			
		||||
    ./bin/pleroma_ctl search.meilisearch show-keys <your master key here>
 | 
			
		||||
    ```
 | 
			
		||||
 | 
			
		||||
=== "From Source"
 | 
			
		||||
    ```sh
 | 
			
		||||
    mix pleroma.search.meilisearch show-keys <your master key here>
 | 
			
		||||
    ```
 | 
			
		||||
 | 
			
		||||
You will see a "Default Admin API Key", this is the key you actually put into your configuration file.
 | 
			
		||||
 | 
			
		||||
### Initial indexing
 | 
			
		||||
 | 
			
		||||
After setting up the configuration, you'll want to index all of your already existsing posts. Only public posts are indexed.  You'll only
 | 
			
		||||
have to do it one time, but it might take a while, depending on the amount of posts your instance has seen. This is also a fairly RAM
 | 
			
		||||
consuming process for `meilisearch`, and it will take a lot of RAM when running if you have a lot of posts (seems to be around 5G for ~1.2
 | 
			
		||||
million posts while idle and up to 7G while indexing initially, but your experience may be different).
 | 
			
		||||
 | 
			
		||||
The sequence of actions is as follows:
 | 
			
		||||
 | 
			
		||||
1. First, change the configuration to use `Pleroma.Search.Meilisearch` as the search backend
 | 
			
		||||
2. Restart your instance, at this point it can be used while the search indexing is running, though search won't return anything
 | 
			
		||||
3. Start the initial indexing process (as described below with `index`),
 | 
			
		||||
   and wait until the task says it sent everything from the database to index
 | 
			
		||||
4. Wait until everything is actually indexed (by checking with `stats` as described below),
 | 
			
		||||
   at this point you don't have to do anything, just wait a while.
 | 
			
		||||
 | 
			
		||||
To start the initial indexing, run the `index` command:
 | 
			
		||||
 | 
			
		||||
=== "OTP"
 | 
			
		||||
    ```sh
 | 
			
		||||
    ./bin/pleroma_ctl search.meilisearch index
 | 
			
		||||
    ```
 | 
			
		||||
 | 
			
		||||
=== "From Source"
 | 
			
		||||
    ```sh
 | 
			
		||||
    mix pleroma.search.meilisearch index
 | 
			
		||||
    ```
 | 
			
		||||
 | 
			
		||||
This will show you the total amount of posts to index, and then show you the amount of posts indexed currently, until the numbers eventually
 | 
			
		||||
become the same. The posts are indexed in big batches and meilisearch will take some time to actually index them, even after you have
 | 
			
		||||
inserted all the posts into it. Depending on the amount of posts, this may be as long as several hours. To get information about the status
 | 
			
		||||
of indexing and how many posts have actually been indexed, use the `stats` command:
 | 
			
		||||
 | 
			
		||||
=== "OTP"
 | 
			
		||||
    ```sh
 | 
			
		||||
    ./bin/pleroma_ctl search.meilisearch stats
 | 
			
		||||
    ```
 | 
			
		||||
 | 
			
		||||
=== "From Source"
 | 
			
		||||
    ```sh
 | 
			
		||||
    mix pleroma.search.meilisearch stats
 | 
			
		||||
    ```
 | 
			
		||||
 | 
			
		||||
### Clearing the index
 | 
			
		||||
 | 
			
		||||
In case you need to clear the index (for example, to re-index from scratch, if that needs to happen for some reason), you can
 | 
			
		||||
use the `clear` command:
 | 
			
		||||
 | 
			
		||||
=== "OTP"
 | 
			
		||||
    ```sh
 | 
			
		||||
    ./bin/pleroma_ctl search.meilisearch clear
 | 
			
		||||
    ```
 | 
			
		||||
 | 
			
		||||
=== "From Source"
 | 
			
		||||
    ```sh
 | 
			
		||||
    mix pleroma.search.meilisearch clear
 | 
			
		||||
    ```
 | 
			
		||||
 | 
			
		||||
This will clear **all** the posts from the search index. Note, that deleted posts are also removed from index by the instance itself, so
 | 
			
		||||
there is no need to actually clear the whole index, unless you want **all** of it gone. That said, the index does not hold any information
 | 
			
		||||
that cannot be re-created from the database, it should also generally be a lot smaller than the size of your database. Still, the size
 | 
			
		||||
depends on the amount of text in posts.
 | 
			
		||||
 | 
			
		||||
## Elasticsearch
 | 
			
		||||
 | 
			
		||||
As with meilisearch, this can be rather memory-hungry, but it is very good at what it does.
 | 
			
		||||
 | 
			
		||||
To use [elasticsearch](https://www.elastic.co/), set the search module to `Pleroma.Search.Elasticsearch`:
 | 
			
		||||
 | 
			
		||||
> config :pleroma, Pleroma.Search, module: Pleroma.Search.Elasticsearch
 | 
			
		||||
 | 
			
		||||
You then need to set the URL and authentication credentials if relevant.
 | 
			
		||||
 | 
			
		||||
> config :pleroma, Pleroma.Search.Elasticsearch.Cluster,
 | 
			
		||||
>    url: "http://127.0.0.1:9200/",
 | 
			
		||||
>    username: "elastic",
 | 
			
		||||
>    password: "changeme",
 | 
			
		||||
 | 
			
		||||
### Initial indexing
 | 
			
		||||
 | 
			
		||||
After setting up the configuration, you'll want to index all of your already existsing posts. Only public posts are indexed.  You'll only
 | 
			
		||||
have to do it one time, but it might take a while, depending on the amount of posts your instance has seen. 
 | 
			
		||||
 | 
			
		||||
The sequence of actions is as follows:
 | 
			
		||||
 | 
			
		||||
1. First, change the configuration to use `Pleroma.Search.Elasticsearch` as the search backend
 | 
			
		||||
2. Restart your instance, at this point it can be used while the search indexing is running, though search won't return anything
 | 
			
		||||
3. Start the initial indexing process (as described below with `index`),
 | 
			
		||||
   and wait until the task says it sent everything from the database to index
 | 
			
		||||
4. Wait until the index tasks exits
 | 
			
		||||
 | 
			
		||||
To start the initial indexing, run the `build` command:
 | 
			
		||||
 | 
			
		||||
=== "OTP"
 | 
			
		||||
```sh
 | 
			
		||||
./bin/pleroma_ctl search import activities
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
=== "From Source"
 | 
			
		||||
```sh
 | 
			
		||||
mix pleroma.search import activities
 | 
			
		||||
```
 | 
			
		||||
| 
						 | 
				
			
			@ -241,6 +241,7 @@ Additional parameters can be added to the JSON body/Form data:
 | 
			
		|||
- `discoverable` - if true, external services (search bots) etc. are allowed to index / list the account (regardless of this setting, user will still appear in regular search results).
 | 
			
		||||
- `actor_type` - the type of this account.
 | 
			
		||||
- `accepts_chat_messages` - if false, this account will reject all chat messages.
 | 
			
		||||
- `language` - user's preferred language for receiving emails (digest, confirmation, etc.)
 | 
			
		||||
 | 
			
		||||
All images (avatar, banner and background) can be reset to the default by sending an empty string ("") instead of a file.
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -292,6 +293,7 @@ Has these additional parameters (which are the same as in Pleroma-API):
 | 
			
		|||
- `captcha_token`: optional, contains provider-specific captcha token
 | 
			
		||||
- `captcha_answer_data`: optional, contains provider-specific captcha data
 | 
			
		||||
- `token`: invite token required when the registrations aren't public.
 | 
			
		||||
- `language`: optional, user's preferred language for receiving emails (digest, confirmation, etc.), default to the language set in the `userLanguage` cookies or `Accept-Language` header.
 | 
			
		||||
 | 
			
		||||
## Instance
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -57,7 +57,8 @@ def start_pleroma do
 | 
			
		|||
        {Majic.Pool,
 | 
			
		||||
         [name: Pleroma.MajicPool, pool_size: Pleroma.Config.get([:majic_pool, :size], 2)]}
 | 
			
		||||
      ] ++
 | 
			
		||||
        http_children(adapter)
 | 
			
		||||
        http_children(adapter) ++
 | 
			
		||||
        elasticsearch_children()
 | 
			
		||||
 | 
			
		||||
    cachex_children = Enum.map(@cachex_children, &Pleroma.Application.build_cachex(&1, []))
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -136,4 +137,14 @@ defp http_children(Tesla.Adapter.Gun) do
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  defp http_children(_), do: []
 | 
			
		||||
 | 
			
		||||
  def elasticsearch_children do
 | 
			
		||||
    config = Pleroma.Config.get([Pleroma.Search, :module])
 | 
			
		||||
 | 
			
		||||
    if config == Pleroma.Search.Elasticsearch do
 | 
			
		||||
      [Pleroma.Search.Elasticsearch.Cluster]
 | 
			
		||||
    else
 | 
			
		||||
      []
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,60 +5,16 @@
 | 
			
		|||
defmodule Mix.Tasks.Pleroma.Search do
 | 
			
		||||
  use Mix.Task
 | 
			
		||||
  import Mix.Pleroma
 | 
			
		||||
  import Ecto.Query
 | 
			
		||||
  alias Pleroma.Activity
 | 
			
		||||
  alias Pleroma.Pagination
 | 
			
		||||
  alias Pleroma.User
 | 
			
		||||
  alias Pleroma.Hashtag
 | 
			
		||||
 | 
			
		||||
  @shortdoc "Manages elasticsearch"
 | 
			
		||||
 | 
			
		||||
  def run(["import", "activities" | _rest]) do
 | 
			
		||||
    start_pleroma()
 | 
			
		||||
 | 
			
		||||
    from(a in Activity, where: not ilike(a.actor, "%/relay"))
 | 
			
		||||
    |> where([a], fragment("(? ->> 'type'::text) = 'Create'", a.data))
 | 
			
		||||
    |> Activity.with_preloaded_object()
 | 
			
		||||
    |> Activity.with_preloaded_user_actor()
 | 
			
		||||
    |> get_all(:activities)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def run(["import", "users" | _rest]) do
 | 
			
		||||
    start_pleroma()
 | 
			
		||||
 | 
			
		||||
    from(u in User, where: u.nickname not in ["internal.fetch", "relay"])
 | 
			
		||||
    |> get_all(:users)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def run(["import", "hashtags" | _rest]) do
 | 
			
		||||
    start_pleroma()
 | 
			
		||||
 | 
			
		||||
    from(h in Hashtag)
 | 
			
		||||
    |> Pleroma.Repo.all()
 | 
			
		||||
    |> Pleroma.Elasticsearch.bulk_post(:hashtags)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp get_all(query, index, max_id \\ nil) do
 | 
			
		||||
    params = %{limit: 1000}
 | 
			
		||||
 | 
			
		||||
    params =
 | 
			
		||||
      if max_id == nil do
 | 
			
		||||
        params
 | 
			
		||||
      else
 | 
			
		||||
        Map.put(params, :max_id, max_id)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    res =
 | 
			
		||||
      query
 | 
			
		||||
      |> Pagination.fetch_paginated(params)
 | 
			
		||||
 | 
			
		||||
    if res == [] do
 | 
			
		||||
      :ok
 | 
			
		||||
    else
 | 
			
		||||
      res
 | 
			
		||||
      |> Pleroma.Elasticsearch.bulk_post(index)
 | 
			
		||||
 | 
			
		||||
      get_all(query, index, List.last(res).id)
 | 
			
		||||
    end
 | 
			
		||||
    Elasticsearch.Index.Bulk.upload(
 | 
			
		||||
      Pleroma.Search.Elasticsearch.Cluster,
 | 
			
		||||
      "activities",
 | 
			
		||||
      Pleroma.Config.get([Pleroma.Search.Elasticsearch.Cluster, :indexes, :activities])
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										144
									
								
								lib/mix/tasks/pleroma/search/meilisearch.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								lib/mix/tasks/pleroma/search/meilisearch.ex
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,144 @@
 | 
			
		|||
# Pleroma: A lightweight social networking server
 | 
			
		||||
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
 | 
			
		||||
# SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
defmodule Mix.Tasks.Pleroma.Search.Meilisearch do
 | 
			
		||||
  require Pleroma.Constants
 | 
			
		||||
 | 
			
		||||
  import Mix.Pleroma
 | 
			
		||||
  import Ecto.Query
 | 
			
		||||
 | 
			
		||||
  import Pleroma.Search.Meilisearch,
 | 
			
		||||
    only: [meili_post: 2, meili_put: 2, meili_get: 1, meili_delete!: 1]
 | 
			
		||||
 | 
			
		||||
  def run(["index"]) do
 | 
			
		||||
    start_pleroma()
 | 
			
		||||
 | 
			
		||||
    meili_version =
 | 
			
		||||
      (
 | 
			
		||||
        {:ok, result} = meili_get("/version")
 | 
			
		||||
 | 
			
		||||
        result["pkgVersion"]
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
    # The ranking rule syntax was changed but nothing about that is mentioned in the changelog
 | 
			
		||||
    if not Version.match?(meili_version, ">= 0.25.0") do
 | 
			
		||||
      raise "Meilisearch <0.24.0 not supported"
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    {:ok, _} =
 | 
			
		||||
      meili_post(
 | 
			
		||||
        "/indexes/objects/settings/ranking-rules",
 | 
			
		||||
        [
 | 
			
		||||
          "published:desc",
 | 
			
		||||
          "words",
 | 
			
		||||
          "exactness",
 | 
			
		||||
          "proximity",
 | 
			
		||||
          "typo",
 | 
			
		||||
          "attribute",
 | 
			
		||||
          "sort"
 | 
			
		||||
        ]
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
    {:ok, _} =
 | 
			
		||||
      meili_post(
 | 
			
		||||
        "/indexes/objects/settings/searchable-attributes",
 | 
			
		||||
        [
 | 
			
		||||
          "content"
 | 
			
		||||
        ]
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
    IO.puts("Created indices. Starting to insert posts.")
 | 
			
		||||
 | 
			
		||||
    chunk_size = Pleroma.Config.get([Pleroma.Search.Meilisearch, :initial_indexing_chunk_size])
 | 
			
		||||
 | 
			
		||||
    Pleroma.Repo.transaction(
 | 
			
		||||
      fn ->
 | 
			
		||||
        query =
 | 
			
		||||
          from(Pleroma.Object,
 | 
			
		||||
            # Only index public and unlisted posts which are notes and have some text
 | 
			
		||||
            where:
 | 
			
		||||
              fragment("data->>'type' = 'Note'") and
 | 
			
		||||
                (fragment("data->'to' \\? ?", ^Pleroma.Constants.as_public()) or
 | 
			
		||||
                   fragment("data->'cc' \\? ?", ^Pleroma.Constants.as_public())),
 | 
			
		||||
            order_by: [desc: fragment("data->'published'")]
 | 
			
		||||
          )
 | 
			
		||||
 | 
			
		||||
        count = query |> Pleroma.Repo.aggregate(:count, :data)
 | 
			
		||||
        IO.puts("Entries to index: #{count}")
 | 
			
		||||
 | 
			
		||||
        Pleroma.Repo.stream(
 | 
			
		||||
          query,
 | 
			
		||||
          timeout: :infinity
 | 
			
		||||
        )
 | 
			
		||||
        |> Stream.map(&Pleroma.Search.Meilisearch.object_to_search_data/1)
 | 
			
		||||
        |> Stream.filter(fn o -> not is_nil(o) end)
 | 
			
		||||
        |> Stream.chunk_every(chunk_size)
 | 
			
		||||
        |> Stream.transform(0, fn objects, acc ->
 | 
			
		||||
          new_acc = acc + Enum.count(objects)
 | 
			
		||||
 | 
			
		||||
          # Reset to the beginning of the line and rewrite it
 | 
			
		||||
          IO.write("\r")
 | 
			
		||||
          IO.write("Indexed #{new_acc} entries")
 | 
			
		||||
 | 
			
		||||
          {[objects], new_acc}
 | 
			
		||||
        end)
 | 
			
		||||
        |> Stream.each(fn objects ->
 | 
			
		||||
          result =
 | 
			
		||||
            meili_put(
 | 
			
		||||
              "/indexes/objects/documents",
 | 
			
		||||
              objects
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
          with {:ok, res} <- result do
 | 
			
		||||
            if not Map.has_key?(res, "uid") do
 | 
			
		||||
              IO.puts("\nFailed to index: #{inspect(result)}")
 | 
			
		||||
            end
 | 
			
		||||
          else
 | 
			
		||||
            e -> IO.puts("\nFailed to index due to network error: #{inspect(e)}")
 | 
			
		||||
          end
 | 
			
		||||
        end)
 | 
			
		||||
        |> Stream.run()
 | 
			
		||||
      end,
 | 
			
		||||
      timeout: :infinity
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    IO.write("\n")
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def run(["clear"]) do
 | 
			
		||||
    start_pleroma()
 | 
			
		||||
 | 
			
		||||
    meili_delete!("/indexes/objects/documents")
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def run(["show-keys", master_key]) do
 | 
			
		||||
    start_pleroma()
 | 
			
		||||
 | 
			
		||||
    endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url])
 | 
			
		||||
 | 
			
		||||
    {:ok, result} =
 | 
			
		||||
      Pleroma.HTTP.get(
 | 
			
		||||
        Path.join(endpoint, "/keys"),
 | 
			
		||||
        [{"Authorization", "Bearer #{master_key}"}]
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
    decoded = Jason.decode!(result.body)
 | 
			
		||||
 | 
			
		||||
    if decoded["results"] do
 | 
			
		||||
      Enum.each(decoded["results"], fn %{"description" => desc, "key" => key} ->
 | 
			
		||||
        IO.puts("#{desc}: #{key}")
 | 
			
		||||
      end)
 | 
			
		||||
    else
 | 
			
		||||
      IO.puts("Error fetching the keys, check the master key is correct: #{inspect(decoded)}")
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def run(["stats"]) do
 | 
			
		||||
    start_pleroma()
 | 
			
		||||
 | 
			
		||||
    {:ok, result} = meili_get("/indexes/objects/stats")
 | 
			
		||||
    IO.puts("Number of entries: #{result["numberOfDocuments"]}")
 | 
			
		||||
    IO.puts("Indexing? #{result["isIndexing"]}")
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -367,7 +367,7 @@ def restrict_deactivated_users(query) do
 | 
			
		|||
    from(activity in query, where: activity.actor not in subquery(deactivated_users_query))
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defdelegate search(user, query, options \\ []), to: Pleroma.Activity.Search
 | 
			
		||||
  defdelegate search(user, query, options \\ []), to: Pleroma.Search.DatabaseSearch
 | 
			
		||||
 | 
			
		||||
  def direct_conversation_id(activity, for_user) do
 | 
			
		||||
    alias Pleroma.Conversation.Participation
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -105,6 +105,7 @@ def start(_type, _args) do
 | 
			
		|||
          {Oban, Config.get(Oban)},
 | 
			
		||||
          Pleroma.Web.Endpoint
 | 
			
		||||
        ] ++
 | 
			
		||||
        elasticsearch_children() ++
 | 
			
		||||
        task_children(@mix_env) ++
 | 
			
		||||
        dont_run_in_test(@mix_env) ++
 | 
			
		||||
        shout_child(shout_enabled?())
 | 
			
		||||
| 
						 | 
				
			
			@ -303,11 +304,25 @@ defp http_children(Tesla.Adapter.Gun, _) do
 | 
			
		|||
 | 
			
		||||
  defp http_children(_, _), do: []
 | 
			
		||||
 | 
			
		||||
  def elasticsearch_children do
 | 
			
		||||
    config = Config.get([Pleroma.Search, :module])
 | 
			
		||||
 | 
			
		||||
    if config == Pleroma.Search.Elasticsearch do
 | 
			
		||||
      [Pleroma.Search.Elasticsearch.Cluster]
 | 
			
		||||
    else
 | 
			
		||||
      []
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @spec limiters_setup() :: :ok
 | 
			
		||||
  def limiters_setup do
 | 
			
		||||
    config = Config.get(ConcurrentLimiter, [])
 | 
			
		||||
 | 
			
		||||
    [Pleroma.Web.RichMedia.Helpers, Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy]
 | 
			
		||||
    [
 | 
			
		||||
      Pleroma.Web.RichMedia.Helpers,
 | 
			
		||||
      Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy,
 | 
			
		||||
      Pleroma.Search
 | 
			
		||||
    ]
 | 
			
		||||
    |> Enum.each(fn module ->
 | 
			
		||||
      mod_config = Keyword.get(config, module, [])
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,19 +0,0 @@
 | 
			
		|||
# Akkoma: A lightweight social networking server
 | 
			
		||||
# Copyright © 2022-2022 Akkoma Authors <https://git.ihatebeinga.live/IHBAGang/akkoma/>
 | 
			
		||||
# SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
defmodule Pleroma.Elasticsearch.DocumentMappings.Activity do
 | 
			
		||||
  alias Pleroma.Object
 | 
			
		||||
 | 
			
		||||
  def id(obj), do: obj.id
 | 
			
		||||
 | 
			
		||||
  def encode(%{object: %{data: %{"type" => "Note"}}} = activity) do
 | 
			
		||||
    %{
 | 
			
		||||
      _timestamp: activity.inserted_at,
 | 
			
		||||
      user: activity.user_actor.nickname,
 | 
			
		||||
      content: activity.object.data["content"],
 | 
			
		||||
      instance: URI.parse(activity.user_actor.ap_id).host,
 | 
			
		||||
      hashtags: Object.hashtags(activity.object)
 | 
			
		||||
    }
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -1,21 +0,0 @@
 | 
			
		|||
# Akkoma: A lightweight social networking server
 | 
			
		||||
# Copyright © 2022-2022 Akkoma Authors <https://git.ihatebeinga.live/IHBAGang/akkoma/>
 | 
			
		||||
# SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
defmodule Pleroma.Elasticsearch.DocumentMappings.Hashtag do
 | 
			
		||||
  def id(obj), do: obj.id
 | 
			
		||||
 | 
			
		||||
  def encode(%{timestamp: _} = hashtag) do
 | 
			
		||||
    %{
 | 
			
		||||
      hashtag: hashtag.name,
 | 
			
		||||
      timestamp: hashtag.timestamp
 | 
			
		||||
    }
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def encode(hashtag) do
 | 
			
		||||
    %{
 | 
			
		||||
      hashtag: hashtag.name,
 | 
			
		||||
      timestamp: hashtag.inserted_at
 | 
			
		||||
    }
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -1,17 +0,0 @@
 | 
			
		|||
# Akkoma: A lightweight social networking server
 | 
			
		||||
# Copyright © 2022-2022 Akkoma Authors <https://git.ihatebeinga.live/IHBAGang/akkoma/>
 | 
			
		||||
# SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
defmodule Pleroma.Elasticsearch.DocumentMappings.User do
 | 
			
		||||
  def id(obj), do: obj.id
 | 
			
		||||
 | 
			
		||||
  def encode(%{actor_type: "Person"} = user) do
 | 
			
		||||
    %{
 | 
			
		||||
      timestamp: user.inserted_at,
 | 
			
		||||
      instance: URI.parse(user.ap_id).host,
 | 
			
		||||
      nickname: user.nickname,
 | 
			
		||||
      bio: user.bio,
 | 
			
		||||
      display_name: user.name
 | 
			
		||||
    }
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -1,256 +0,0 @@
 | 
			
		|||
# Akkoma: A lightweight social networking server
 | 
			
		||||
# Copyright © 2022-2022 Akkoma Authors <https://git.ihatebeinga.live/IHBAGang/akkoma/>
 | 
			
		||||
# SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
defmodule Pleroma.Elasticsearch do
 | 
			
		||||
  alias Pleroma.Activity
 | 
			
		||||
  alias Pleroma.User
 | 
			
		||||
  alias Pleroma.Object
 | 
			
		||||
  alias Pleroma.Elasticsearch.DocumentMappings
 | 
			
		||||
  alias Pleroma.Config
 | 
			
		||||
  require Logger
 | 
			
		||||
 | 
			
		||||
  defp url do
 | 
			
		||||
    Config.get([:elasticsearch, :url])
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp enabled? do
 | 
			
		||||
    Config.get([:search, :provider]) == Pleroma.Search.Elasticsearch
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def delete_by_id(:activity, id) do
 | 
			
		||||
    if enabled?() do
 | 
			
		||||
      Elastix.Document.delete(url(), "activities", "activity", id)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def put_by_id(:activity, id) do
 | 
			
		||||
    id
 | 
			
		||||
    |> Activity.get_by_id_with_object()
 | 
			
		||||
    |> maybe_put_into_elasticsearch()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def maybe_put_into_elasticsearch({:ok, item}) do
 | 
			
		||||
    maybe_put_into_elasticsearch(item)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def maybe_put_into_elasticsearch(
 | 
			
		||||
        %{data: %{"type" => "Create"}, object: %{data: %{"type" => "Note"}}} = activity
 | 
			
		||||
      ) do
 | 
			
		||||
    if enabled?() do
 | 
			
		||||
      actor = Pleroma.Activity.user_actor(activity)
 | 
			
		||||
 | 
			
		||||
      activity
 | 
			
		||||
      |> Map.put(:user_actor, actor)
 | 
			
		||||
      |> put()
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def maybe_put_into_elasticsearch(%User{actor_type: "Person"} = user) do
 | 
			
		||||
    if enabled?() do
 | 
			
		||||
      put(user)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def maybe_put_into_elasticsearch(_) do
 | 
			
		||||
    {:ok, :skipped}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def maybe_bulk_post(data, type) do
 | 
			
		||||
    if enabled?() do
 | 
			
		||||
      bulk_post(data, type)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def put(%Activity{} = activity) do
 | 
			
		||||
    with {:ok, _} <-
 | 
			
		||||
           Elastix.Document.index(
 | 
			
		||||
             url(),
 | 
			
		||||
             "activities",
 | 
			
		||||
             "activity",
 | 
			
		||||
             DocumentMappings.Activity.id(activity),
 | 
			
		||||
             DocumentMappings.Activity.encode(activity)
 | 
			
		||||
           ) do
 | 
			
		||||
      activity
 | 
			
		||||
      |> Map.get(:object)
 | 
			
		||||
      |> Object.hashtags()
 | 
			
		||||
      |> Enum.map(fn x ->
 | 
			
		||||
        %{id: x, name: x, timestamp: DateTime.to_iso8601(DateTime.utc_now())}
 | 
			
		||||
      end)
 | 
			
		||||
      |> bulk_post(:hashtags)
 | 
			
		||||
    else
 | 
			
		||||
      {:error, %{reason: err}} ->
 | 
			
		||||
        Logger.error("Could not put activity: #{err}")
 | 
			
		||||
        :skipped
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def put(%User{} = user) do
 | 
			
		||||
    with {:ok, _} <-
 | 
			
		||||
           Elastix.Document.index(
 | 
			
		||||
             url(),
 | 
			
		||||
             "users",
 | 
			
		||||
             "user",
 | 
			
		||||
             DocumentMappings.User.id(user),
 | 
			
		||||
             DocumentMappings.User.encode(user)
 | 
			
		||||
           ) do
 | 
			
		||||
      :ok
 | 
			
		||||
    else
 | 
			
		||||
      {:error, %{reason: err}} ->
 | 
			
		||||
        Logger.error("Could not put user: #{err}")
 | 
			
		||||
        :skipped
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def bulk_post(data, :activities) do
 | 
			
		||||
    d =
 | 
			
		||||
      data
 | 
			
		||||
      |> Enum.filter(fn x ->
 | 
			
		||||
        t =
 | 
			
		||||
          x.object
 | 
			
		||||
          |> Map.get(:data, %{})
 | 
			
		||||
          |> Map.get("type", "")
 | 
			
		||||
 | 
			
		||||
        t == "Note"
 | 
			
		||||
      end)
 | 
			
		||||
      |> Enum.map(fn d ->
 | 
			
		||||
        [
 | 
			
		||||
          %{index: %{_id: DocumentMappings.Activity.id(d)}},
 | 
			
		||||
          DocumentMappings.Activity.encode(d)
 | 
			
		||||
        ]
 | 
			
		||||
      end)
 | 
			
		||||
      |> List.flatten()
 | 
			
		||||
 | 
			
		||||
    with {:ok, %{body: %{"errors" => false}}} <-
 | 
			
		||||
           Elastix.Bulk.post(
 | 
			
		||||
             url(),
 | 
			
		||||
             d,
 | 
			
		||||
             index: "activities",
 | 
			
		||||
             type: "activity"
 | 
			
		||||
           ) do
 | 
			
		||||
      :ok
 | 
			
		||||
    else
 | 
			
		||||
      {:error, %{reason: err}} ->
 | 
			
		||||
        Logger.error("Could not bulk put activity: #{err}")
 | 
			
		||||
        :skipped
 | 
			
		||||
 | 
			
		||||
      {:ok, %{body: _}} ->
 | 
			
		||||
        :skipped
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def bulk_post(data, :users) do
 | 
			
		||||
    d =
 | 
			
		||||
      data
 | 
			
		||||
      |> Enum.filter(fn x -> x.actor_type == "Person" end)
 | 
			
		||||
      |> Enum.map(fn d ->
 | 
			
		||||
        [
 | 
			
		||||
          %{index: %{_id: DocumentMappings.User.id(d)}},
 | 
			
		||||
          DocumentMappings.User.encode(d)
 | 
			
		||||
        ]
 | 
			
		||||
      end)
 | 
			
		||||
      |> List.flatten()
 | 
			
		||||
 | 
			
		||||
    with {:ok, %{body: %{"errors" => false}}} <-
 | 
			
		||||
           Elastix.Bulk.post(
 | 
			
		||||
             url(),
 | 
			
		||||
             d,
 | 
			
		||||
             index: "users",
 | 
			
		||||
             type: "user"
 | 
			
		||||
           ) do
 | 
			
		||||
      :ok
 | 
			
		||||
    else
 | 
			
		||||
      {:error, %{reason: err}} ->
 | 
			
		||||
        Logger.error("Could not bulk put users: #{err}")
 | 
			
		||||
        :skipped
 | 
			
		||||
 | 
			
		||||
      {:ok, %{body: _}} ->
 | 
			
		||||
        :skipped
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def bulk_post(data, :hashtags) when is_list(data) do
 | 
			
		||||
    d =
 | 
			
		||||
      data
 | 
			
		||||
      |> Enum.map(fn d ->
 | 
			
		||||
        [
 | 
			
		||||
          %{index: %{_id: DocumentMappings.Hashtag.id(d)}},
 | 
			
		||||
          DocumentMappings.Hashtag.encode(d)
 | 
			
		||||
        ]
 | 
			
		||||
      end)
 | 
			
		||||
      |> List.flatten()
 | 
			
		||||
 | 
			
		||||
    with {:ok, %{body: %{"errors" => false}}} <-
 | 
			
		||||
           Elastix.Bulk.post(
 | 
			
		||||
             url(),
 | 
			
		||||
             d,
 | 
			
		||||
             index: "hashtags",
 | 
			
		||||
             type: "hashtag"
 | 
			
		||||
           ) do
 | 
			
		||||
      :ok
 | 
			
		||||
    else
 | 
			
		||||
      {:error, %{reason: err}} ->
 | 
			
		||||
        Logger.error("Could not bulk put hashtags: #{err}")
 | 
			
		||||
        :skipped
 | 
			
		||||
 | 
			
		||||
      {:ok, %{body: _}} ->
 | 
			
		||||
        :skipped
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def bulk_post(_, :hashtags), do: {:ok, nil}
 | 
			
		||||
 | 
			
		||||
  def search(_, _, _, :skip), do: []
 | 
			
		||||
 | 
			
		||||
  def search(:raw, index, type, q) do
 | 
			
		||||
    with {:ok, raw_results} <- Elastix.Search.search(url(), index, [type], q) do
 | 
			
		||||
      results =
 | 
			
		||||
        raw_results
 | 
			
		||||
        |> Map.get(:body, %{})
 | 
			
		||||
        |> Map.get("hits", %{})
 | 
			
		||||
        |> Map.get("hits", [])
 | 
			
		||||
 | 
			
		||||
      {:ok, results}
 | 
			
		||||
    else
 | 
			
		||||
      {:error, e} ->
 | 
			
		||||
        Logger.error(e)
 | 
			
		||||
        {:error, e}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def search(:activities, q) do
 | 
			
		||||
    with {:ok, results} <- search(:raw, "activities", "activity", q) do
 | 
			
		||||
      results
 | 
			
		||||
      |> Enum.map(fn result -> result["_id"] end)
 | 
			
		||||
      |> Pleroma.Activity.all_by_ids_with_object()
 | 
			
		||||
      |> Enum.sort(&(&1.inserted_at >= &2.inserted_at))
 | 
			
		||||
    else
 | 
			
		||||
      e ->
 | 
			
		||||
        Logger.error(e)
 | 
			
		||||
        []
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def search(:users, q) do
 | 
			
		||||
    with {:ok, results} <- search(:raw, "users", "user", q) do
 | 
			
		||||
      results
 | 
			
		||||
      |> Enum.map(fn result -> result["_id"] end)
 | 
			
		||||
      |> Pleroma.User.get_all_by_ids()
 | 
			
		||||
    else
 | 
			
		||||
      e ->
 | 
			
		||||
        Logger.error(e)
 | 
			
		||||
        []
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def search(:hashtags, q) do
 | 
			
		||||
    with {:ok, results} <- search(:raw, "hashtags", "hashtag", q) do
 | 
			
		||||
      results
 | 
			
		||||
      |> Enum.map(fn result -> result["_source"]["hashtag"] end)
 | 
			
		||||
    else
 | 
			
		||||
      e ->
 | 
			
		||||
        Logger.error(e)
 | 
			
		||||
        []
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -5,9 +5,12 @@
 | 
			
		|||
defmodule Pleroma.Emails.UserEmail do
 | 
			
		||||
  @moduledoc "User emails"
 | 
			
		||||
 | 
			
		||||
  require Pleroma.Web.Gettext
 | 
			
		||||
 | 
			
		||||
  alias Pleroma.Config
 | 
			
		||||
  alias Pleroma.User
 | 
			
		||||
  alias Pleroma.Web.Endpoint
 | 
			
		||||
  alias Pleroma.Web.Gettext
 | 
			
		||||
  alias Pleroma.Web.Router
 | 
			
		||||
 | 
			
		||||
  import Swoosh.Email
 | 
			
		||||
| 
						 | 
				
			
			@ -27,29 +30,75 @@ defp recipient(%User{} = user), do: recipient(user.email, user.name)
 | 
			
		|||
 | 
			
		||||
  @spec welcome(User.t(), map()) :: Swoosh.Email.t()
 | 
			
		||||
  def welcome(user, opts \\ %{}) do
 | 
			
		||||
    new()
 | 
			
		||||
    |> to(recipient(user))
 | 
			
		||||
    |> from(Map.get(opts, :sender, sender()))
 | 
			
		||||
    |> subject(Map.get(opts, :subject, "Welcome to #{instance_name()}!"))
 | 
			
		||||
    |> html_body(Map.get(opts, :html, "Welcome to #{instance_name()}!"))
 | 
			
		||||
    |> text_body(Map.get(opts, :text, "Welcome to #{instance_name()}!"))
 | 
			
		||||
    Gettext.with_locale_or_default user.language do
 | 
			
		||||
      new()
 | 
			
		||||
      |> to(recipient(user))
 | 
			
		||||
      |> from(Map.get(opts, :sender, sender()))
 | 
			
		||||
      |> subject(
 | 
			
		||||
        Map.get(
 | 
			
		||||
          opts,
 | 
			
		||||
          :subject,
 | 
			
		||||
          Gettext.dpgettext(
 | 
			
		||||
            "static_pages",
 | 
			
		||||
            "welcome email subject",
 | 
			
		||||
            "Welcome to %{instance_name}!",
 | 
			
		||||
            instance_name: instance_name()
 | 
			
		||||
          )
 | 
			
		||||
        )
 | 
			
		||||
      )
 | 
			
		||||
      |> html_body(
 | 
			
		||||
        Map.get(
 | 
			
		||||
          opts,
 | 
			
		||||
          :html,
 | 
			
		||||
          Gettext.dpgettext(
 | 
			
		||||
            "static_pages",
 | 
			
		||||
            "welcome email html body",
 | 
			
		||||
            "Welcome to %{instance_name}!",
 | 
			
		||||
            instance_name: instance_name()
 | 
			
		||||
          )
 | 
			
		||||
        )
 | 
			
		||||
      )
 | 
			
		||||
      |> text_body(
 | 
			
		||||
        Map.get(
 | 
			
		||||
          opts,
 | 
			
		||||
          :text,
 | 
			
		||||
          Gettext.dpgettext(
 | 
			
		||||
            "static_pages",
 | 
			
		||||
            "welcome email text body",
 | 
			
		||||
            "Welcome to %{instance_name}!",
 | 
			
		||||
            instance_name: instance_name()
 | 
			
		||||
          )
 | 
			
		||||
        )
 | 
			
		||||
      )
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def password_reset_email(user, token) when is_binary(token) do
 | 
			
		||||
    password_reset_url = Router.Helpers.reset_password_url(Endpoint, :reset, token)
 | 
			
		||||
    Gettext.with_locale_or_default user.language do
 | 
			
		||||
      password_reset_url = Router.Helpers.reset_password_url(Endpoint, :reset, token)
 | 
			
		||||
 | 
			
		||||
    html_body = """
 | 
			
		||||
    <h3>Reset your password at #{instance_name()}</h3>
 | 
			
		||||
    <p>Someone has requested password change for your account at #{instance_name()}.</p>
 | 
			
		||||
    <p>If it was you, visit the following link to proceed: <a href="#{password_reset_url}">reset password</a>.</p>
 | 
			
		||||
    <p>If it was someone else, nothing to worry about: your data is secure and your password has not been changed.</p>
 | 
			
		||||
    """
 | 
			
		||||
      html_body =
 | 
			
		||||
        Gettext.dpgettext(
 | 
			
		||||
          "static_pages",
 | 
			
		||||
          "password reset email body",
 | 
			
		||||
          """
 | 
			
		||||
          <h3>Reset your password at %{instance_name}</h3>
 | 
			
		||||
          <p>Someone has requested password change for your account at %{instance_name}.</p>
 | 
			
		||||
          <p>If it was you, visit the following link to proceed: <a href="%{password_reset_url}">reset password</a>.</p>
 | 
			
		||||
          <p>If it was someone else, nothing to worry about: your data is secure and your password has not been changed.</p>
 | 
			
		||||
          """,
 | 
			
		||||
          instance_name: instance_name(),
 | 
			
		||||
          password_reset_url: password_reset_url
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    new()
 | 
			
		||||
    |> to(recipient(user))
 | 
			
		||||
    |> from(sender())
 | 
			
		||||
    |> subject("Password reset")
 | 
			
		||||
    |> html_body(html_body)
 | 
			
		||||
      new()
 | 
			
		||||
      |> to(recipient(user))
 | 
			
		||||
      |> from(sender())
 | 
			
		||||
      |> subject(
 | 
			
		||||
        Gettext.dpgettext("static_pages", "password reset email subject", "Password reset")
 | 
			
		||||
      )
 | 
			
		||||
      |> html_body(html_body)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def user_invitation_email(
 | 
			
		||||
| 
						 | 
				
			
			@ -58,73 +107,136 @@ def user_invitation_email(
 | 
			
		|||
        to_email,
 | 
			
		||||
        to_name \\ nil
 | 
			
		||||
      ) do
 | 
			
		||||
    registration_url =
 | 
			
		||||
      Router.Helpers.redirect_url(
 | 
			
		||||
        Endpoint,
 | 
			
		||||
        :registration_page,
 | 
			
		||||
        user_invite_token.token
 | 
			
		||||
    Gettext.with_locale_or_default user.language do
 | 
			
		||||
      registration_url =
 | 
			
		||||
        Router.Helpers.redirect_url(
 | 
			
		||||
          Endpoint,
 | 
			
		||||
          :registration_page,
 | 
			
		||||
          user_invite_token.token
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
      html_body =
 | 
			
		||||
        Gettext.dpgettext(
 | 
			
		||||
          "static_pages",
 | 
			
		||||
          "user invitation email body",
 | 
			
		||||
          """
 | 
			
		||||
          <h3>You are invited to %{instance_name}</h3>
 | 
			
		||||
          <p>%{inviter_name} invites you to join %{instance_name}, an instance of Pleroma federated social networking platform.</p>
 | 
			
		||||
          <p>Click the following link to register: <a href="%{registration_url}">accept invitation</a>.</p>
 | 
			
		||||
          """,
 | 
			
		||||
          instance_name: instance_name(),
 | 
			
		||||
          inviter_name: user.name,
 | 
			
		||||
          registration_url: registration_url
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
      new()
 | 
			
		||||
      |> to(recipient(to_email, to_name))
 | 
			
		||||
      |> from(sender())
 | 
			
		||||
      |> subject(
 | 
			
		||||
        Gettext.dpgettext(
 | 
			
		||||
          "static_pages",
 | 
			
		||||
          "user invitation email subject",
 | 
			
		||||
          "Invitation to %{instance_name}",
 | 
			
		||||
          instance_name: instance_name()
 | 
			
		||||
        )
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
    html_body = """
 | 
			
		||||
    <h3>You are invited to #{instance_name()}</h3>
 | 
			
		||||
    <p>#{user.name} invites you to join #{instance_name()}, an instance of Pleroma federated social networking platform.</p>
 | 
			
		||||
    <p>Click the following link to register: <a href="#{registration_url}">accept invitation</a>.</p>
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    new()
 | 
			
		||||
    |> to(recipient(to_email, to_name))
 | 
			
		||||
    |> from(sender())
 | 
			
		||||
    |> subject("Invitation to #{instance_name()}")
 | 
			
		||||
    |> html_body(html_body)
 | 
			
		||||
      |> html_body(html_body)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def account_confirmation_email(user) do
 | 
			
		||||
    confirmation_url =
 | 
			
		||||
      Router.Helpers.confirm_email_url(
 | 
			
		||||
        Endpoint,
 | 
			
		||||
        :confirm_email,
 | 
			
		||||
        user.id,
 | 
			
		||||
        to_string(user.confirmation_token)
 | 
			
		||||
    Gettext.with_locale_or_default user.language do
 | 
			
		||||
      confirmation_url =
 | 
			
		||||
        Router.Helpers.confirm_email_url(
 | 
			
		||||
          Endpoint,
 | 
			
		||||
          :confirm_email,
 | 
			
		||||
          user.id,
 | 
			
		||||
          to_string(user.confirmation_token)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
      html_body =
 | 
			
		||||
        Gettext.dpgettext(
 | 
			
		||||
          "static_pages",
 | 
			
		||||
          "confirmation email body",
 | 
			
		||||
          """
 | 
			
		||||
          <h3>Thank you for registering on %{instance_name}</h3>
 | 
			
		||||
          <p>Email confirmation is required to activate the account.</p>
 | 
			
		||||
          <p>Please click the following link to <a href="%{confirmation_url}">activate your account</a>.</p>
 | 
			
		||||
          """,
 | 
			
		||||
          instance_name: instance_name(),
 | 
			
		||||
          confirmation_url: confirmation_url
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
      new()
 | 
			
		||||
      |> to(recipient(user))
 | 
			
		||||
      |> from(sender())
 | 
			
		||||
      |> subject(
 | 
			
		||||
        Gettext.dpgettext(
 | 
			
		||||
          "static_pages",
 | 
			
		||||
          "confirmation email subject",
 | 
			
		||||
          "%{instance_name} account confirmation",
 | 
			
		||||
          instance_name: instance_name()
 | 
			
		||||
        )
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
    html_body = """
 | 
			
		||||
    <h3>Thank you for registering on #{instance_name()}</h3>
 | 
			
		||||
    <p>Email confirmation is required to activate the account.</p>
 | 
			
		||||
    <p>Please click the following link to <a href="#{confirmation_url}">activate your account</a>.</p>
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    new()
 | 
			
		||||
    |> to(recipient(user))
 | 
			
		||||
    |> from(sender())
 | 
			
		||||
    |> subject("#{instance_name()} account confirmation")
 | 
			
		||||
    |> html_body(html_body)
 | 
			
		||||
      |> html_body(html_body)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def approval_pending_email(user) do
 | 
			
		||||
    html_body = """
 | 
			
		||||
    <h3>Awaiting Approval</h3>
 | 
			
		||||
    <p>Your account at #{instance_name()} is being reviewed by staff. You will receive another email once your account is approved.</p>
 | 
			
		||||
    """
 | 
			
		||||
    Gettext.with_locale_or_default user.language do
 | 
			
		||||
      html_body =
 | 
			
		||||
        Gettext.dpgettext(
 | 
			
		||||
          "static_pages",
 | 
			
		||||
          "approval pending email body",
 | 
			
		||||
          """
 | 
			
		||||
          <h3>Awaiting Approval</h3>
 | 
			
		||||
          <p>Your account at %{instance_name} is being reviewed by staff. You will receive another email once your account is approved.</p>
 | 
			
		||||
          """,
 | 
			
		||||
          instance_name: instance_name()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    new()
 | 
			
		||||
    |> to(recipient(user))
 | 
			
		||||
    |> from(sender())
 | 
			
		||||
    |> subject("Your account is awaiting approval")
 | 
			
		||||
    |> html_body(html_body)
 | 
			
		||||
      new()
 | 
			
		||||
      |> to(recipient(user))
 | 
			
		||||
      |> from(sender())
 | 
			
		||||
      |> subject(
 | 
			
		||||
        Gettext.dpgettext(
 | 
			
		||||
          "static_pages",
 | 
			
		||||
          "approval pending email subject",
 | 
			
		||||
          "Your account is awaiting approval"
 | 
			
		||||
        )
 | 
			
		||||
      )
 | 
			
		||||
      |> html_body(html_body)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def successful_registration_email(user) do
 | 
			
		||||
    html_body = """
 | 
			
		||||
    <h3>Hello @#{user.nickname},</h3>
 | 
			
		||||
    <p>Your account at #{instance_name()} has been registered successfully.</p>
 | 
			
		||||
    <p>No further action is required to activate your account.</p>
 | 
			
		||||
    """
 | 
			
		||||
    Gettext.with_locale_or_default user.language do
 | 
			
		||||
      html_body =
 | 
			
		||||
        Gettext.dpgettext(
 | 
			
		||||
          "static_pages",
 | 
			
		||||
          "successful registration email body",
 | 
			
		||||
          """
 | 
			
		||||
          <h3>Hello @%{nickname},</h3>
 | 
			
		||||
          <p>Your account at %{instance_name} has been registered successfully.</p>
 | 
			
		||||
          <p>No further action is required to activate your account.</p>
 | 
			
		||||
          """,
 | 
			
		||||
          nickname: user.nickname,
 | 
			
		||||
          instance_name: instance_name()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    new()
 | 
			
		||||
    |> to(recipient(user))
 | 
			
		||||
    |> from(sender())
 | 
			
		||||
    |> subject("Account registered on #{instance_name()}")
 | 
			
		||||
    |> html_body(html_body)
 | 
			
		||||
      new()
 | 
			
		||||
      |> to(recipient(user))
 | 
			
		||||
      |> from(sender())
 | 
			
		||||
      |> subject(
 | 
			
		||||
        Gettext.dpgettext(
 | 
			
		||||
          "static_pages",
 | 
			
		||||
          "successful registration email subject",
 | 
			
		||||
          "Account registered on %{instance_name}",
 | 
			
		||||
          instance_name: instance_name()
 | 
			
		||||
        )
 | 
			
		||||
      )
 | 
			
		||||
      |> html_body(html_body)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
| 
						 | 
				
			
			@ -134,69 +246,78 @@ def successful_registration_email(user) do
 | 
			
		|||
  """
 | 
			
		||||
  @spec digest_email(User.t()) :: Swoosh.Email.t() | nil
 | 
			
		||||
  def digest_email(user) do
 | 
			
		||||
    notifications = Pleroma.Notification.for_user_since(user, user.last_digest_emailed_at)
 | 
			
		||||
    Gettext.with_locale_or_default user.language do
 | 
			
		||||
      notifications = Pleroma.Notification.for_user_since(user, user.last_digest_emailed_at)
 | 
			
		||||
 | 
			
		||||
    mentions =
 | 
			
		||||
      notifications
 | 
			
		||||
      |> Enum.filter(&(&1.activity.data["type"] == "Create"))
 | 
			
		||||
      |> Enum.map(fn notification ->
 | 
			
		||||
        object = Pleroma.Object.normalize(notification.activity, fetch: false)
 | 
			
		||||
      mentions =
 | 
			
		||||
        notifications
 | 
			
		||||
        |> Enum.filter(&(&1.activity.data["type"] == "Create"))
 | 
			
		||||
        |> Enum.map(fn notification ->
 | 
			
		||||
          object = Pleroma.Object.normalize(notification.activity, fetch: false)
 | 
			
		||||
 | 
			
		||||
        if not is_nil(object) do
 | 
			
		||||
          object = update_in(object.data["content"], &format_links/1)
 | 
			
		||||
          if not is_nil(object) do
 | 
			
		||||
            object = update_in(object.data["content"], &format_links/1)
 | 
			
		||||
 | 
			
		||||
          %{
 | 
			
		||||
            data: notification,
 | 
			
		||||
            object: object,
 | 
			
		||||
            from: User.get_by_ap_id(notification.activity.actor)
 | 
			
		||||
          }
 | 
			
		||||
        end
 | 
			
		||||
      end)
 | 
			
		||||
      |> Enum.filter(& &1)
 | 
			
		||||
            %{
 | 
			
		||||
              data: notification,
 | 
			
		||||
              object: object,
 | 
			
		||||
              from: User.get_by_ap_id(notification.activity.actor)
 | 
			
		||||
            }
 | 
			
		||||
          end
 | 
			
		||||
        end)
 | 
			
		||||
        |> Enum.filter(& &1)
 | 
			
		||||
 | 
			
		||||
    followers =
 | 
			
		||||
      notifications
 | 
			
		||||
      |> Enum.filter(&(&1.activity.data["type"] == "Follow"))
 | 
			
		||||
      |> Enum.map(fn notification ->
 | 
			
		||||
        from = User.get_by_ap_id(notification.activity.actor)
 | 
			
		||||
      followers =
 | 
			
		||||
        notifications
 | 
			
		||||
        |> Enum.filter(&(&1.activity.data["type"] == "Follow"))
 | 
			
		||||
        |> Enum.map(fn notification ->
 | 
			
		||||
          from = User.get_by_ap_id(notification.activity.actor)
 | 
			
		||||
 | 
			
		||||
        if not is_nil(from) do
 | 
			
		||||
          %{
 | 
			
		||||
            data: notification,
 | 
			
		||||
            object: Pleroma.Object.normalize(notification.activity, fetch: false),
 | 
			
		||||
            from: User.get_by_ap_id(notification.activity.actor)
 | 
			
		||||
          }
 | 
			
		||||
        end
 | 
			
		||||
      end)
 | 
			
		||||
      |> Enum.filter(& &1)
 | 
			
		||||
          if not is_nil(from) do
 | 
			
		||||
            %{
 | 
			
		||||
              data: notification,
 | 
			
		||||
              object: Pleroma.Object.normalize(notification.activity, fetch: false),
 | 
			
		||||
              from: User.get_by_ap_id(notification.activity.actor)
 | 
			
		||||
            }
 | 
			
		||||
          end
 | 
			
		||||
        end)
 | 
			
		||||
        |> Enum.filter(& &1)
 | 
			
		||||
 | 
			
		||||
    unless Enum.empty?(mentions) do
 | 
			
		||||
      styling = Config.get([__MODULE__, :styling])
 | 
			
		||||
      logo = Config.get([__MODULE__, :logo])
 | 
			
		||||
      unless Enum.empty?(mentions) do
 | 
			
		||||
        styling = Config.get([__MODULE__, :styling])
 | 
			
		||||
        logo = Config.get([__MODULE__, :logo])
 | 
			
		||||
 | 
			
		||||
      html_data = %{
 | 
			
		||||
        instance: instance_name(),
 | 
			
		||||
        user: user,
 | 
			
		||||
        mentions: mentions,
 | 
			
		||||
        followers: followers,
 | 
			
		||||
        unsubscribe_link: unsubscribe_url(user, "digest"),
 | 
			
		||||
        styling: styling
 | 
			
		||||
      }
 | 
			
		||||
        html_data = %{
 | 
			
		||||
          instance: instance_name(),
 | 
			
		||||
          user: user,
 | 
			
		||||
          mentions: mentions,
 | 
			
		||||
          followers: followers,
 | 
			
		||||
          unsubscribe_link: unsubscribe_url(user, "digest"),
 | 
			
		||||
          styling: styling
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
      logo_path =
 | 
			
		||||
        if is_nil(logo) do
 | 
			
		||||
          Path.join(:code.priv_dir(:pleroma), "static/static/logo.svg")
 | 
			
		||||
        else
 | 
			
		||||
          Path.join(Config.get([:instance, :static_dir]), logo)
 | 
			
		||||
        end
 | 
			
		||||
        logo_path =
 | 
			
		||||
          if is_nil(logo) do
 | 
			
		||||
            Path.join(:code.priv_dir(:pleroma), "static/static/logo.svg")
 | 
			
		||||
          else
 | 
			
		||||
            Path.join(Config.get([:instance, :static_dir]), logo)
 | 
			
		||||
          end
 | 
			
		||||
 | 
			
		||||
      new()
 | 
			
		||||
      |> to(recipient(user))
 | 
			
		||||
      |> from(sender())
 | 
			
		||||
      |> subject("Your digest from #{instance_name()}")
 | 
			
		||||
      |> put_layout(false)
 | 
			
		||||
      |> render_body("digest.html", html_data)
 | 
			
		||||
      |> attachment(Swoosh.Attachment.new(logo_path, filename: "logo.svg", type: :inline))
 | 
			
		||||
        new()
 | 
			
		||||
        |> to(recipient(user))
 | 
			
		||||
        |> from(sender())
 | 
			
		||||
        |> subject(
 | 
			
		||||
          Gettext.dpgettext(
 | 
			
		||||
            "static_pages",
 | 
			
		||||
            "digest email subject",
 | 
			
		||||
            "Your digest from %{instance_name}",
 | 
			
		||||
            instance_name: instance_name()
 | 
			
		||||
          )
 | 
			
		||||
        )
 | 
			
		||||
        |> put_layout(false)
 | 
			
		||||
        |> render_body("digest.html", html_data)
 | 
			
		||||
        |> attachment(Swoosh.Attachment.new(logo_path, filename: "logo.svg", type: :inline))
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -226,27 +347,47 @@ def unsubscribe_url(user, notifications_type) do
 | 
			
		|||
 | 
			
		||||
  def backup_is_ready_email(backup, admin_user_id \\ nil) do
 | 
			
		||||
    %{user: user} = Pleroma.Repo.preload(backup, :user)
 | 
			
		||||
    download_url = Pleroma.Web.PleromaAPI.BackupView.download_url(backup)
 | 
			
		||||
 | 
			
		||||
    html_body =
 | 
			
		||||
      if is_nil(admin_user_id) do
 | 
			
		||||
        """
 | 
			
		||||
        <p>You requested a full backup of your Pleroma account. It's ready for download:</p>
 | 
			
		||||
        <p><a href="#{download_url}">#{download_url}</a></p>
 | 
			
		||||
        """
 | 
			
		||||
      else
 | 
			
		||||
        admin = Pleroma.Repo.get(User, admin_user_id)
 | 
			
		||||
    Gettext.with_locale_or_default user.language do
 | 
			
		||||
      download_url = Pleroma.Web.PleromaAPI.BackupView.download_url(backup)
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        <p>Admin @#{admin.nickname} requested a full backup of your Pleroma account. It's ready for download:</p>
 | 
			
		||||
        <p><a href="#{download_url}">#{download_url}</a></p>
 | 
			
		||||
        """
 | 
			
		||||
      end
 | 
			
		||||
      html_body =
 | 
			
		||||
        if is_nil(admin_user_id) do
 | 
			
		||||
          Gettext.dpgettext(
 | 
			
		||||
            "static_pages",
 | 
			
		||||
            "account archive email body - self-requested",
 | 
			
		||||
            """
 | 
			
		||||
            <p>You requested a full backup of your Pleroma account. It's ready for download:</p>
 | 
			
		||||
            <p><a href="%{download_url}">%{download_url}</a></p>
 | 
			
		||||
            """,
 | 
			
		||||
            download_url: download_url
 | 
			
		||||
          )
 | 
			
		||||
        else
 | 
			
		||||
          admin = Pleroma.Repo.get(User, admin_user_id)
 | 
			
		||||
 | 
			
		||||
    new()
 | 
			
		||||
    |> to(recipient(user))
 | 
			
		||||
    |> from(sender())
 | 
			
		||||
    |> subject("Your account archive is ready")
 | 
			
		||||
    |> html_body(html_body)
 | 
			
		||||
          Gettext.dpgettext(
 | 
			
		||||
            "static_pages",
 | 
			
		||||
            "account archive email body - admin requested",
 | 
			
		||||
            """
 | 
			
		||||
            <p>Admin @%{admin_nickname} requested a full backup of your Pleroma account. It's ready for download:</p>
 | 
			
		||||
            <p><a href="%{download_url}">%{download_url}</a></p>
 | 
			
		||||
            """,
 | 
			
		||||
            admin_nickname: admin.nickname,
 | 
			
		||||
            download_url: download_url
 | 
			
		||||
          )
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
      new()
 | 
			
		||||
      |> to(recipient(user))
 | 
			
		||||
      |> from(sender())
 | 
			
		||||
      |> subject(
 | 
			
		||||
        Gettext.dpgettext(
 | 
			
		||||
          "static_pages",
 | 
			
		||||
          "account archive email subject",
 | 
			
		||||
          "Your account archive is ready"
 | 
			
		||||
        )
 | 
			
		||||
      )
 | 
			
		||||
      |> html_body(html_body)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,11 +1,11 @@
 | 
			
		|||
# emoji-test.txt
 | 
			
		||||
# Date: 2020-09-12, 22:19:50 GMT
 | 
			
		||||
# © 2020 Unicode®, Inc.
 | 
			
		||||
# Date: 2021-08-26, 17:22:23 GMT
 | 
			
		||||
# © 2021 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 Keyboard/Display Test Data for UTS #51
 | 
			
		||||
# Version: 13.1
 | 
			
		||||
# Version: 14.0
 | 
			
		||||
#
 | 
			
		||||
# For documentation and usage, see http://www.unicode.org/reports/tr51
 | 
			
		||||
#
 | 
			
		||||
| 
						 | 
				
			
			@ -43,6 +43,7 @@
 | 
			
		|||
1F602                                                  ; fully-qualified     # 😂 E0.6 face with tears of joy
 | 
			
		||||
1F642                                                  ; fully-qualified     # 🙂 E1.0 slightly smiling face
 | 
			
		||||
1F643                                                  ; fully-qualified     # 🙃 E1.0 upside-down face
 | 
			
		||||
1FAE0                                                  ; fully-qualified     # 🫠 E14.0 melting face
 | 
			
		||||
1F609                                                  ; fully-qualified     # 😉 E0.6 winking face
 | 
			
		||||
1F60A                                                  ; fully-qualified     # 😊 E0.6 smiling face with smiling eyes
 | 
			
		||||
1F607                                                  ; fully-qualified     # 😇 E1.0 smiling face with halo
 | 
			
		||||
| 
						 | 
				
			
			@ -68,10 +69,13 @@
 | 
			
		|||
1F911                                                  ; fully-qualified     # 🤑 E1.0 money-mouth face
 | 
			
		||||
 | 
			
		||||
# subgroup: face-hand
 | 
			
		||||
1F917                                                  ; fully-qualified     # 🤗 E1.0 hugging face
 | 
			
		||||
1F917                                                  ; fully-qualified     # 🤗 E1.0 smiling face with open hands
 | 
			
		||||
1F92D                                                  ; fully-qualified     # 🤭 E5.0 face with hand over mouth
 | 
			
		||||
1FAE2                                                  ; fully-qualified     # 🫢 E14.0 face with open eyes and hand over mouth
 | 
			
		||||
1FAE3                                                  ; fully-qualified     # 🫣 E14.0 face with peeking eye
 | 
			
		||||
1F92B                                                  ; fully-qualified     # 🤫 E5.0 shushing face
 | 
			
		||||
1F914                                                  ; fully-qualified     # 🤔 E1.0 thinking face
 | 
			
		||||
1FAE1                                                  ; fully-qualified     # 🫡 E14.0 saluting face
 | 
			
		||||
 | 
			
		||||
# subgroup: face-neutral-skeptical
 | 
			
		||||
1F910                                                  ; fully-qualified     # 🤐 E1.0 zipper-mouth face
 | 
			
		||||
| 
						 | 
				
			
			@ -79,6 +83,7 @@
 | 
			
		|||
1F610                                                  ; fully-qualified     # 😐 E0.7 neutral face
 | 
			
		||||
1F611                                                  ; fully-qualified     # 😑 E1.0 expressionless face
 | 
			
		||||
1F636                                                  ; fully-qualified     # 😶 E1.0 face without mouth
 | 
			
		||||
1FAE5                                                  ; fully-qualified     # 🫥 E14.0 dotted line face
 | 
			
		||||
1F636 200D 1F32B FE0F                                  ; fully-qualified     # 😶🌫️ E13.1 face in clouds
 | 
			
		||||
1F636 200D 1F32B                                       ; minimally-qualified # 😶🌫 E13.1 face in clouds
 | 
			
		||||
1F60F                                                  ; fully-qualified     # 😏 E0.6 smirking face
 | 
			
		||||
| 
						 | 
				
			
			@ -105,7 +110,7 @@
 | 
			
		|||
1F975                                                  ; fully-qualified     # 🥵 E11.0 hot face
 | 
			
		||||
1F976                                                  ; fully-qualified     # 🥶 E11.0 cold face
 | 
			
		||||
1F974                                                  ; fully-qualified     # 🥴 E11.0 woozy face
 | 
			
		||||
1F635                                                  ; fully-qualified     # 😵 E0.6 knocked-out face
 | 
			
		||||
1F635                                                  ; fully-qualified     # 😵 E0.6 face with crossed-out eyes
 | 
			
		||||
1F635 200D 1F4AB                                       ; fully-qualified     # 😵💫 E13.1 face with spiral eyes
 | 
			
		||||
1F92F                                                  ; fully-qualified     # 🤯 E5.0 exploding head
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -121,6 +126,7 @@
 | 
			
		|||
 | 
			
		||||
# subgroup: face-concerned
 | 
			
		||||
1F615                                                  ; fully-qualified     # 😕 E1.0 confused face
 | 
			
		||||
1FAE4                                                  ; fully-qualified     # 🫤 E14.0 face with diagonal mouth
 | 
			
		||||
1F61F                                                  ; fully-qualified     # 😟 E1.0 worried face
 | 
			
		||||
1F641                                                  ; fully-qualified     # 🙁 E1.0 slightly frowning face
 | 
			
		||||
2639 FE0F                                              ; fully-qualified     # ☹️ E0.7 frowning face
 | 
			
		||||
| 
						 | 
				
			
			@ -130,6 +136,7 @@
 | 
			
		|||
1F632                                                  ; fully-qualified     # 😲 E0.6 astonished face
 | 
			
		||||
1F633                                                  ; fully-qualified     # 😳 E0.6 flushed face
 | 
			
		||||
1F97A                                                  ; fully-qualified     # 🥺 E11.0 pleading face
 | 
			
		||||
1F979                                                  ; fully-qualified     # 🥹 E14.0 face holding back tears
 | 
			
		||||
1F626                                                  ; fully-qualified     # 😦 E1.0 frowning face with open mouth
 | 
			
		||||
1F627                                                  ; fully-qualified     # 😧 E1.0 anguished face
 | 
			
		||||
1F628                                                  ; fully-qualified     # 😨 E0.6 fearful face
 | 
			
		||||
| 
						 | 
				
			
			@ -232,8 +239,8 @@
 | 
			
		|||
1F4AD                                                  ; fully-qualified     # 💭 E1.0 thought balloon
 | 
			
		||||
1F4A4                                                  ; fully-qualified     # 💤 E0.6 zzz
 | 
			
		||||
 | 
			
		||||
# Smileys & Emotion subtotal:		170
 | 
			
		||||
# Smileys & Emotion subtotal:		170	w/o modifiers
 | 
			
		||||
# Smileys & Emotion subtotal:		177
 | 
			
		||||
# Smileys & Emotion subtotal:		177	w/o modifiers
 | 
			
		||||
 | 
			
		||||
# group: People & Body
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -269,6 +276,30 @@
 | 
			
		|||
1F596 1F3FD                                            ; fully-qualified     # 🖖🏽 E1.0 vulcan salute: medium skin tone
 | 
			
		||||
1F596 1F3FE                                            ; fully-qualified     # 🖖🏾 E1.0 vulcan salute: medium-dark skin tone
 | 
			
		||||
1F596 1F3FF                                            ; fully-qualified     # 🖖🏿 E1.0 vulcan salute: dark skin tone
 | 
			
		||||
1FAF1                                                  ; fully-qualified     # 🫱 E14.0 rightwards hand
 | 
			
		||||
1FAF1 1F3FB                                            ; fully-qualified     # 🫱🏻 E14.0 rightwards hand: light skin tone
 | 
			
		||||
1FAF1 1F3FC                                            ; fully-qualified     # 🫱🏼 E14.0 rightwards hand: medium-light skin tone
 | 
			
		||||
1FAF1 1F3FD                                            ; fully-qualified     # 🫱🏽 E14.0 rightwards hand: medium skin tone
 | 
			
		||||
1FAF1 1F3FE                                            ; fully-qualified     # 🫱🏾 E14.0 rightwards hand: medium-dark skin tone
 | 
			
		||||
1FAF1 1F3FF                                            ; fully-qualified     # 🫱🏿 E14.0 rightwards hand: dark skin tone
 | 
			
		||||
1FAF2                                                  ; fully-qualified     # 🫲 E14.0 leftwards hand
 | 
			
		||||
1FAF2 1F3FB                                            ; fully-qualified     # 🫲🏻 E14.0 leftwards hand: light skin tone
 | 
			
		||||
1FAF2 1F3FC                                            ; fully-qualified     # 🫲🏼 E14.0 leftwards hand: medium-light skin tone
 | 
			
		||||
1FAF2 1F3FD                                            ; fully-qualified     # 🫲🏽 E14.0 leftwards hand: medium skin tone
 | 
			
		||||
1FAF2 1F3FE                                            ; fully-qualified     # 🫲🏾 E14.0 leftwards hand: medium-dark skin tone
 | 
			
		||||
1FAF2 1F3FF                                            ; fully-qualified     # 🫲🏿 E14.0 leftwards hand: dark skin tone
 | 
			
		||||
1FAF3                                                  ; fully-qualified     # 🫳 E14.0 palm down hand
 | 
			
		||||
1FAF3 1F3FB                                            ; fully-qualified     # 🫳🏻 E14.0 palm down hand: light skin tone
 | 
			
		||||
1FAF3 1F3FC                                            ; fully-qualified     # 🫳🏼 E14.0 palm down hand: medium-light skin tone
 | 
			
		||||
1FAF3 1F3FD                                            ; fully-qualified     # 🫳🏽 E14.0 palm down hand: medium skin tone
 | 
			
		||||
1FAF3 1F3FE                                            ; fully-qualified     # 🫳🏾 E14.0 palm down hand: medium-dark skin tone
 | 
			
		||||
1FAF3 1F3FF                                            ; fully-qualified     # 🫳🏿 E14.0 palm down hand: dark skin tone
 | 
			
		||||
1FAF4                                                  ; fully-qualified     # 🫴 E14.0 palm up hand
 | 
			
		||||
1FAF4 1F3FB                                            ; fully-qualified     # 🫴🏻 E14.0 palm up hand: light skin tone
 | 
			
		||||
1FAF4 1F3FC                                            ; fully-qualified     # 🫴🏼 E14.0 palm up hand: medium-light skin tone
 | 
			
		||||
1FAF4 1F3FD                                            ; fully-qualified     # 🫴🏽 E14.0 palm up hand: medium skin tone
 | 
			
		||||
1FAF4 1F3FE                                            ; fully-qualified     # 🫴🏾 E14.0 palm up hand: medium-dark skin tone
 | 
			
		||||
1FAF4 1F3FF                                            ; fully-qualified     # 🫴🏿 E14.0 palm up hand: dark skin tone
 | 
			
		||||
 | 
			
		||||
# subgroup: hand-fingers-partial
 | 
			
		||||
1F44C                                                  ; fully-qualified     # 👌 E0.6 OK hand
 | 
			
		||||
| 
						 | 
				
			
			@ -302,6 +333,12 @@
 | 
			
		|||
1F91E 1F3FD                                            ; fully-qualified     # 🤞🏽 E3.0 crossed fingers: medium skin tone
 | 
			
		||||
1F91E 1F3FE                                            ; fully-qualified     # 🤞🏾 E3.0 crossed fingers: medium-dark skin tone
 | 
			
		||||
1F91E 1F3FF                                            ; fully-qualified     # 🤞🏿 E3.0 crossed fingers: dark skin tone
 | 
			
		||||
1FAF0                                                  ; fully-qualified     # 🫰 E14.0 hand with index finger and thumb crossed
 | 
			
		||||
1FAF0 1F3FB                                            ; fully-qualified     # 🫰🏻 E14.0 hand with index finger and thumb crossed: light skin tone
 | 
			
		||||
1FAF0 1F3FC                                            ; fully-qualified     # 🫰🏼 E14.0 hand with index finger and thumb crossed: medium-light skin tone
 | 
			
		||||
1FAF0 1F3FD                                            ; fully-qualified     # 🫰🏽 E14.0 hand with index finger and thumb crossed: medium skin tone
 | 
			
		||||
1FAF0 1F3FE                                            ; fully-qualified     # 🫰🏾 E14.0 hand with index finger and thumb crossed: medium-dark skin tone
 | 
			
		||||
1FAF0 1F3FF                                            ; fully-qualified     # 🫰🏿 E14.0 hand with index finger and thumb crossed: dark skin tone
 | 
			
		||||
1F91F                                                  ; fully-qualified     # 🤟 E5.0 love-you gesture
 | 
			
		||||
1F91F 1F3FB                                            ; fully-qualified     # 🤟🏻 E5.0 love-you gesture: light skin tone
 | 
			
		||||
1F91F 1F3FC                                            ; fully-qualified     # 🤟🏼 E5.0 love-you gesture: medium-light skin tone
 | 
			
		||||
| 
						 | 
				
			
			@ -359,6 +396,12 @@
 | 
			
		|||
261D 1F3FD                                             ; fully-qualified     # ☝🏽 E1.0 index pointing up: medium skin tone
 | 
			
		||||
261D 1F3FE                                             ; fully-qualified     # ☝🏾 E1.0 index pointing up: medium-dark skin tone
 | 
			
		||||
261D 1F3FF                                             ; fully-qualified     # ☝🏿 E1.0 index pointing up: dark skin tone
 | 
			
		||||
1FAF5                                                  ; fully-qualified     # 🫵 E14.0 index pointing at the viewer
 | 
			
		||||
1FAF5 1F3FB                                            ; fully-qualified     # 🫵🏻 E14.0 index pointing at the viewer: light skin tone
 | 
			
		||||
1FAF5 1F3FC                                            ; fully-qualified     # 🫵🏼 E14.0 index pointing at the viewer: medium-light skin tone
 | 
			
		||||
1FAF5 1F3FD                                            ; fully-qualified     # 🫵🏽 E14.0 index pointing at the viewer: medium skin tone
 | 
			
		||||
1FAF5 1F3FE                                            ; fully-qualified     # 🫵🏾 E14.0 index pointing at the viewer: medium-dark skin tone
 | 
			
		||||
1FAF5 1F3FF                                            ; fully-qualified     # 🫵🏿 E14.0 index pointing at the viewer: dark skin tone
 | 
			
		||||
 | 
			
		||||
# subgroup: hand-fingers-closed
 | 
			
		||||
1F44D                                                  ; fully-qualified     # 👍 E0.6 thumbs up
 | 
			
		||||
| 
						 | 
				
			
			@ -411,6 +454,12 @@
 | 
			
		|||
1F64C 1F3FD                                            ; fully-qualified     # 🙌🏽 E1.0 raising hands: medium skin tone
 | 
			
		||||
1F64C 1F3FE                                            ; fully-qualified     # 🙌🏾 E1.0 raising hands: medium-dark skin tone
 | 
			
		||||
1F64C 1F3FF                                            ; fully-qualified     # 🙌🏿 E1.0 raising hands: dark skin tone
 | 
			
		||||
1FAF6                                                  ; fully-qualified     # 🫶 E14.0 heart hands
 | 
			
		||||
1FAF6 1F3FB                                            ; fully-qualified     # 🫶🏻 E14.0 heart hands: light skin tone
 | 
			
		||||
1FAF6 1F3FC                                            ; fully-qualified     # 🫶🏼 E14.0 heart hands: medium-light skin tone
 | 
			
		||||
1FAF6 1F3FD                                            ; fully-qualified     # 🫶🏽 E14.0 heart hands: medium skin tone
 | 
			
		||||
1FAF6 1F3FE                                            ; fully-qualified     # 🫶🏾 E14.0 heart hands: medium-dark skin tone
 | 
			
		||||
1FAF6 1F3FF                                            ; fully-qualified     # 🫶🏿 E14.0 heart hands: dark skin tone
 | 
			
		||||
1F450                                                  ; fully-qualified     # 👐 E0.6 open hands
 | 
			
		||||
1F450 1F3FB                                            ; fully-qualified     # 👐🏻 E1.0 open hands: light skin tone
 | 
			
		||||
1F450 1F3FC                                            ; fully-qualified     # 👐🏼 E1.0 open hands: medium-light skin tone
 | 
			
		||||
| 
						 | 
				
			
			@ -424,6 +473,31 @@
 | 
			
		|||
1F932 1F3FE                                            ; fully-qualified     # 🤲🏾 E5.0 palms up together: medium-dark skin tone
 | 
			
		||||
1F932 1F3FF                                            ; fully-qualified     # 🤲🏿 E5.0 palms up together: dark skin tone
 | 
			
		||||
1F91D                                                  ; fully-qualified     # 🤝 E3.0 handshake
 | 
			
		||||
1F91D 1F3FB                                            ; fully-qualified     # 🤝🏻 E3.0 handshake: light skin tone
 | 
			
		||||
1F91D 1F3FC                                            ; fully-qualified     # 🤝🏼 E3.0 handshake: medium-light skin tone
 | 
			
		||||
1F91D 1F3FD                                            ; fully-qualified     # 🤝🏽 E3.0 handshake: medium skin tone
 | 
			
		||||
1F91D 1F3FE                                            ; fully-qualified     # 🤝🏾 E3.0 handshake: medium-dark skin tone
 | 
			
		||||
1F91D 1F3FF                                            ; fully-qualified     # 🤝🏿 E3.0 handshake: dark skin tone
 | 
			
		||||
1FAF1 1F3FB 200D 1FAF2 1F3FC                           ; fully-qualified     # 🫱🏻🫲🏼 E14.0 handshake: light skin tone, medium-light skin tone
 | 
			
		||||
1FAF1 1F3FB 200D 1FAF2 1F3FD                           ; fully-qualified     # 🫱🏻🫲🏽 E14.0 handshake: light skin tone, medium skin tone
 | 
			
		||||
1FAF1 1F3FB 200D 1FAF2 1F3FE                           ; fully-qualified     # 🫱🏻🫲🏾 E14.0 handshake: light skin tone, medium-dark skin tone
 | 
			
		||||
1FAF1 1F3FB 200D 1FAF2 1F3FF                           ; fully-qualified     # 🫱🏻🫲🏿 E14.0 handshake: light skin tone, dark skin tone
 | 
			
		||||
1FAF1 1F3FC 200D 1FAF2 1F3FB                           ; fully-qualified     # 🫱🏼🫲🏻 E14.0 handshake: medium-light skin tone, light skin tone
 | 
			
		||||
1FAF1 1F3FC 200D 1FAF2 1F3FD                           ; fully-qualified     # 🫱🏼🫲🏽 E14.0 handshake: medium-light skin tone, medium skin tone
 | 
			
		||||
1FAF1 1F3FC 200D 1FAF2 1F3FE                           ; fully-qualified     # 🫱🏼🫲🏾 E14.0 handshake: medium-light skin tone, medium-dark skin tone
 | 
			
		||||
1FAF1 1F3FC 200D 1FAF2 1F3FF                           ; fully-qualified     # 🫱🏼🫲🏿 E14.0 handshake: medium-light skin tone, dark skin tone
 | 
			
		||||
1FAF1 1F3FD 200D 1FAF2 1F3FB                           ; fully-qualified     # 🫱🏽🫲🏻 E14.0 handshake: medium skin tone, light skin tone
 | 
			
		||||
1FAF1 1F3FD 200D 1FAF2 1F3FC                           ; fully-qualified     # 🫱🏽🫲🏼 E14.0 handshake: medium skin tone, medium-light skin tone
 | 
			
		||||
1FAF1 1F3FD 200D 1FAF2 1F3FE                           ; fully-qualified     # 🫱🏽🫲🏾 E14.0 handshake: medium skin tone, medium-dark skin tone
 | 
			
		||||
1FAF1 1F3FD 200D 1FAF2 1F3FF                           ; fully-qualified     # 🫱🏽🫲🏿 E14.0 handshake: medium skin tone, dark skin tone
 | 
			
		||||
1FAF1 1F3FE 200D 1FAF2 1F3FB                           ; fully-qualified     # 🫱🏾🫲🏻 E14.0 handshake: medium-dark skin tone, light skin tone
 | 
			
		||||
1FAF1 1F3FE 200D 1FAF2 1F3FC                           ; fully-qualified     # 🫱🏾🫲🏼 E14.0 handshake: medium-dark skin tone, medium-light skin tone
 | 
			
		||||
1FAF1 1F3FE 200D 1FAF2 1F3FD                           ; fully-qualified     # 🫱🏾🫲🏽 E14.0 handshake: medium-dark skin tone, medium skin tone
 | 
			
		||||
1FAF1 1F3FE 200D 1FAF2 1F3FF                           ; fully-qualified     # 🫱🏾🫲🏿 E14.0 handshake: medium-dark skin tone, dark skin tone
 | 
			
		||||
1FAF1 1F3FF 200D 1FAF2 1F3FB                           ; fully-qualified     # 🫱🏿🫲🏻 E14.0 handshake: dark skin tone, light skin tone
 | 
			
		||||
1FAF1 1F3FF 200D 1FAF2 1F3FC                           ; fully-qualified     # 🫱🏿🫲🏼 E14.0 handshake: dark skin tone, medium-light skin tone
 | 
			
		||||
1FAF1 1F3FF 200D 1FAF2 1F3FD                           ; fully-qualified     # 🫱🏿🫲🏽 E14.0 handshake: dark skin tone, medium skin tone
 | 
			
		||||
1FAF1 1F3FF 200D 1FAF2 1F3FE                           ; fully-qualified     # 🫱🏿🫲🏾 E14.0 handshake: dark skin tone, medium-dark skin tone
 | 
			
		||||
1F64F                                                  ; fully-qualified     # 🙏 E0.6 folded hands
 | 
			
		||||
1F64F 1F3FB                                            ; fully-qualified     # 🙏🏻 E1.0 folded hands: light skin tone
 | 
			
		||||
1F64F 1F3FC                                            ; fully-qualified     # 🙏🏼 E1.0 folded hands: medium-light skin tone
 | 
			
		||||
| 
						 | 
				
			
			@ -501,6 +575,7 @@
 | 
			
		|||
1F441                                                  ; unqualified         # 👁 E0.7 eye
 | 
			
		||||
1F445                                                  ; fully-qualified     # 👅 E0.6 tongue
 | 
			
		||||
1F444                                                  ; fully-qualified     # 👄 E0.6 mouth
 | 
			
		||||
1FAE6                                                  ; fully-qualified     # 🫦 E14.0 biting lip
 | 
			
		||||
 | 
			
		||||
# subgroup: person
 | 
			
		||||
1F476                                                  ; fully-qualified     # 👶 E0.6 baby
 | 
			
		||||
| 
						 | 
				
			
			@ -1472,6 +1547,12 @@
 | 
			
		|||
1F477 1F3FE 200D 2640                                  ; minimally-qualified # 👷🏾♀ E4.0 woman construction worker: medium-dark skin tone
 | 
			
		||||
1F477 1F3FF 200D 2640 FE0F                             ; fully-qualified     # 👷🏿♀️ E4.0 woman construction worker: dark skin tone
 | 
			
		||||
1F477 1F3FF 200D 2640                                  ; minimally-qualified # 👷🏿♀ E4.0 woman construction worker: dark skin tone
 | 
			
		||||
1FAC5                                                  ; fully-qualified     # 🫅 E14.0 person with crown
 | 
			
		||||
1FAC5 1F3FB                                            ; fully-qualified     # 🫅🏻 E14.0 person with crown: light skin tone
 | 
			
		||||
1FAC5 1F3FC                                            ; fully-qualified     # 🫅🏼 E14.0 person with crown: medium-light skin tone
 | 
			
		||||
1FAC5 1F3FD                                            ; fully-qualified     # 🫅🏽 E14.0 person with crown: medium skin tone
 | 
			
		||||
1FAC5 1F3FE                                            ; fully-qualified     # 🫅🏾 E14.0 person with crown: medium-dark skin tone
 | 
			
		||||
1FAC5 1F3FF                                            ; fully-qualified     # 🫅🏿 E14.0 person with crown: dark skin tone
 | 
			
		||||
1F934                                                  ; fully-qualified     # 🤴 E3.0 prince
 | 
			
		||||
1F934 1F3FB                                            ; fully-qualified     # 🤴🏻 E3.0 prince: light skin tone
 | 
			
		||||
1F934 1F3FC                                            ; fully-qualified     # 🤴🏼 E3.0 prince: medium-light skin tone
 | 
			
		||||
| 
						 | 
				
			
			@ -1592,6 +1673,18 @@
 | 
			
		|||
1F930 1F3FD                                            ; fully-qualified     # 🤰🏽 E3.0 pregnant woman: medium skin tone
 | 
			
		||||
1F930 1F3FE                                            ; fully-qualified     # 🤰🏾 E3.0 pregnant woman: medium-dark skin tone
 | 
			
		||||
1F930 1F3FF                                            ; fully-qualified     # 🤰🏿 E3.0 pregnant woman: dark skin tone
 | 
			
		||||
1FAC3                                                  ; fully-qualified     # 🫃 E14.0 pregnant man
 | 
			
		||||
1FAC3 1F3FB                                            ; fully-qualified     # 🫃🏻 E14.0 pregnant man: light skin tone
 | 
			
		||||
1FAC3 1F3FC                                            ; fully-qualified     # 🫃🏼 E14.0 pregnant man: medium-light skin tone
 | 
			
		||||
1FAC3 1F3FD                                            ; fully-qualified     # 🫃🏽 E14.0 pregnant man: medium skin tone
 | 
			
		||||
1FAC3 1F3FE                                            ; fully-qualified     # 🫃🏾 E14.0 pregnant man: medium-dark skin tone
 | 
			
		||||
1FAC3 1F3FF                                            ; fully-qualified     # 🫃🏿 E14.0 pregnant man: dark skin tone
 | 
			
		||||
1FAC4                                                  ; fully-qualified     # 🫄 E14.0 pregnant person
 | 
			
		||||
1FAC4 1F3FB                                            ; fully-qualified     # 🫄🏻 E14.0 pregnant person: light skin tone
 | 
			
		||||
1FAC4 1F3FC                                            ; fully-qualified     # 🫄🏼 E14.0 pregnant person: medium-light skin tone
 | 
			
		||||
1FAC4 1F3FD                                            ; fully-qualified     # 🫄🏽 E14.0 pregnant person: medium skin tone
 | 
			
		||||
1FAC4 1F3FE                                            ; fully-qualified     # 🫄🏾 E14.0 pregnant person: medium-dark skin tone
 | 
			
		||||
1FAC4 1F3FF                                            ; fully-qualified     # 🫄🏿 E14.0 pregnant person: dark skin tone
 | 
			
		||||
1F931                                                  ; fully-qualified     # 🤱 E5.0 breast-feeding
 | 
			
		||||
1F931 1F3FB                                            ; fully-qualified     # 🤱🏻 E5.0 breast-feeding: light skin tone
 | 
			
		||||
1F931 1F3FC                                            ; fully-qualified     # 🤱🏼 E5.0 breast-feeding: medium-light skin tone
 | 
			
		||||
| 
						 | 
				
			
			@ -1862,6 +1955,7 @@
 | 
			
		|||
1F9DF 200D 2642                                        ; minimally-qualified # 🧟♂ E5.0 man zombie
 | 
			
		||||
1F9DF 200D 2640 FE0F                                   ; fully-qualified     # 🧟♀️ E5.0 woman zombie
 | 
			
		||||
1F9DF 200D 2640                                        ; minimally-qualified # 🧟♀ E5.0 woman zombie
 | 
			
		||||
1F9CC                                                  ; fully-qualified     # 🧌 E14.0 troll
 | 
			
		||||
 | 
			
		||||
# subgroup: person-activity
 | 
			
		||||
1F486                                                  ; fully-qualified     # 💆 E0.6 person getting massage
 | 
			
		||||
| 
						 | 
				
			
			@ -3168,8 +3262,8 @@
 | 
			
		|||
1FAC2                                                  ; fully-qualified     # 🫂 E13.0 people hugging
 | 
			
		||||
1F463                                                  ; fully-qualified     # 👣 E0.6 footprints
 | 
			
		||||
 | 
			
		||||
# People & Body subtotal:		2899
 | 
			
		||||
# People & Body subtotal:		494	w/o modifiers
 | 
			
		||||
# People & Body subtotal:		2986
 | 
			
		||||
# People & Body subtotal:		506	w/o modifiers
 | 
			
		||||
 | 
			
		||||
# group: Component
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -3304,6 +3398,7 @@
 | 
			
		|||
1F988                                                  ; fully-qualified     # 🦈 E3.0 shark
 | 
			
		||||
1F419                                                  ; fully-qualified     # 🐙 E0.6 octopus
 | 
			
		||||
1F41A                                                  ; fully-qualified     # 🐚 E0.6 spiral shell
 | 
			
		||||
1FAB8                                                  ; fully-qualified     # 🪸 E14.0 coral
 | 
			
		||||
 | 
			
		||||
# subgroup: animal-bug
 | 
			
		||||
1F40C                                                  ; fully-qualified     # 🐌 E0.6 snail
 | 
			
		||||
| 
						 | 
				
			
			@ -3329,6 +3424,7 @@
 | 
			
		|||
1F490                                                  ; fully-qualified     # 💐 E0.6 bouquet
 | 
			
		||||
1F338                                                  ; fully-qualified     # 🌸 E0.6 cherry blossom
 | 
			
		||||
1F4AE                                                  ; fully-qualified     # 💮 E0.6 white flower
 | 
			
		||||
1FAB7                                                  ; fully-qualified     # 🪷 E14.0 lotus
 | 
			
		||||
1F3F5 FE0F                                             ; fully-qualified     # 🏵️ E0.7 rosette
 | 
			
		||||
1F3F5                                                  ; unqualified         # 🏵 E0.7 rosette
 | 
			
		||||
1F339                                                  ; fully-qualified     # 🌹 E0.6 rose
 | 
			
		||||
| 
						 | 
				
			
			@ -3353,9 +3449,11 @@
 | 
			
		|||
1F341                                                  ; fully-qualified     # 🍁 E0.6 maple leaf
 | 
			
		||||
1F342                                                  ; fully-qualified     # 🍂 E0.6 fallen leaf
 | 
			
		||||
1F343                                                  ; fully-qualified     # 🍃 E0.6 leaf fluttering in wind
 | 
			
		||||
1FAB9                                                  ; fully-qualified     # 🪹 E14.0 empty nest
 | 
			
		||||
1FABA                                                  ; fully-qualified     # 🪺 E14.0 nest with eggs
 | 
			
		||||
 | 
			
		||||
# Animals & Nature subtotal:		147
 | 
			
		||||
# Animals & Nature subtotal:		147	w/o modifiers
 | 
			
		||||
# Animals & Nature subtotal:		151
 | 
			
		||||
# Animals & Nature subtotal:		151	w/o modifiers
 | 
			
		||||
 | 
			
		||||
# group: Food & Drink
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -3396,6 +3494,7 @@
 | 
			
		|||
1F9C5                                                  ; fully-qualified     # 🧅 E12.0 onion
 | 
			
		||||
1F344                                                  ; fully-qualified     # 🍄 E0.6 mushroom
 | 
			
		||||
1F95C                                                  ; fully-qualified     # 🥜 E3.0 peanuts
 | 
			
		||||
1FAD8                                                  ; fully-qualified     # 🫘 E14.0 beans
 | 
			
		||||
1F330                                                  ; fully-qualified     # 🌰 E0.6 chestnut
 | 
			
		||||
 | 
			
		||||
# subgroup: food-prepared
 | 
			
		||||
| 
						 | 
				
			
			@ -3491,6 +3590,7 @@
 | 
			
		|||
1F37B                                                  ; fully-qualified     # 🍻 E0.6 clinking beer mugs
 | 
			
		||||
1F942                                                  ; fully-qualified     # 🥂 E3.0 clinking glasses
 | 
			
		||||
1F943                                                  ; fully-qualified     # 🥃 E3.0 tumbler glass
 | 
			
		||||
1FAD7                                                  ; fully-qualified     # 🫗 E14.0 pouring liquid
 | 
			
		||||
1F964                                                  ; fully-qualified     # 🥤 E5.0 cup with straw
 | 
			
		||||
1F9CB                                                  ; fully-qualified     # 🧋 E13.0 bubble tea
 | 
			
		||||
1F9C3                                                  ; fully-qualified     # 🧃 E12.0 beverage box
 | 
			
		||||
| 
						 | 
				
			
			@ -3504,10 +3604,11 @@
 | 
			
		|||
1F374                                                  ; fully-qualified     # 🍴 E0.6 fork and knife
 | 
			
		||||
1F944                                                  ; fully-qualified     # 🥄 E3.0 spoon
 | 
			
		||||
1F52A                                                  ; fully-qualified     # 🔪 E0.6 kitchen knife
 | 
			
		||||
1FAD9                                                  ; fully-qualified     # 🫙 E14.0 jar
 | 
			
		||||
1F3FA                                                  ; fully-qualified     # 🏺 E1.0 amphora
 | 
			
		||||
 | 
			
		||||
# Food & Drink subtotal:		131
 | 
			
		||||
# Food & Drink subtotal:		131	w/o modifiers
 | 
			
		||||
# Food & Drink subtotal:		134
 | 
			
		||||
# Food & Drink subtotal:		134	w/o modifiers
 | 
			
		||||
 | 
			
		||||
# group: Travel & Places
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -3597,6 +3698,7 @@
 | 
			
		|||
2668 FE0F                                              ; fully-qualified     # ♨️ E0.6 hot springs
 | 
			
		||||
2668                                                   ; unqualified         # ♨ E0.6 hot springs
 | 
			
		||||
1F3A0                                                  ; fully-qualified     # 🎠 E0.6 carousel horse
 | 
			
		||||
1F6DD                                                  ; fully-qualified     # 🛝 E14.0 playground slide
 | 
			
		||||
1F3A1                                                  ; fully-qualified     # 🎡 E0.6 ferris wheel
 | 
			
		||||
1F3A2                                                  ; fully-qualified     # 🎢 E0.6 roller coaster
 | 
			
		||||
1F488                                                  ; fully-qualified     # 💈 E0.6 barber pole
 | 
			
		||||
| 
						 | 
				
			
			@ -3652,6 +3754,7 @@
 | 
			
		|||
1F6E2 FE0F                                             ; fully-qualified     # 🛢️ E0.7 oil drum
 | 
			
		||||
1F6E2                                                  ; unqualified         # 🛢 E0.7 oil drum
 | 
			
		||||
26FD                                                   ; fully-qualified     # ⛽ E0.6 fuel pump
 | 
			
		||||
1F6DE                                                  ; fully-qualified     # 🛞 E14.0 wheel
 | 
			
		||||
1F6A8                                                  ; fully-qualified     # 🚨 E0.6 police car light
 | 
			
		||||
1F6A5                                                  ; fully-qualified     # 🚥 E0.6 horizontal traffic light
 | 
			
		||||
1F6A6                                                  ; fully-qualified     # 🚦 E1.0 vertical traffic light
 | 
			
		||||
| 
						 | 
				
			
			@ -3660,6 +3763,7 @@
 | 
			
		|||
 | 
			
		||||
# subgroup: transport-water
 | 
			
		||||
2693                                                   ; fully-qualified     # ⚓ E0.6 anchor
 | 
			
		||||
1F6DF                                                  ; fully-qualified     # 🛟 E14.0 ring buoy
 | 
			
		||||
26F5                                                   ; fully-qualified     # ⛵ E0.6 sailboat
 | 
			
		||||
1F6F6                                                  ; fully-qualified     # 🛶 E3.0 canoe
 | 
			
		||||
1F6A4                                                  ; fully-qualified     # 🚤 E0.6 speedboat
 | 
			
		||||
| 
						 | 
				
			
			@ -3797,8 +3901,8 @@
 | 
			
		|||
1F4A7                                                  ; fully-qualified     # 💧 E0.6 droplet
 | 
			
		||||
1F30A                                                  ; fully-qualified     # 🌊 E0.6 water wave
 | 
			
		||||
 | 
			
		||||
# Travel & Places subtotal:		264
 | 
			
		||||
# Travel & Places subtotal:		264	w/o modifiers
 | 
			
		||||
# Travel & Places subtotal:		267
 | 
			
		||||
# Travel & Places subtotal:		267	w/o modifiers
 | 
			
		||||
 | 
			
		||||
# group: Activities
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -3874,6 +3978,7 @@
 | 
			
		|||
1F52E                                                  ; fully-qualified     # 🔮 E0.6 crystal ball
 | 
			
		||||
1FA84                                                  ; fully-qualified     # 🪄 E13.0 magic wand
 | 
			
		||||
1F9FF                                                  ; fully-qualified     # 🧿 E11.0 nazar amulet
 | 
			
		||||
1FAAC                                                  ; fully-qualified     # 🪬 E14.0 hamsa
 | 
			
		||||
1F3AE                                                  ; fully-qualified     # 🎮 E0.6 video game
 | 
			
		||||
1F579 FE0F                                             ; fully-qualified     # 🕹️ E0.7 joystick
 | 
			
		||||
1F579                                                  ; unqualified         # 🕹 E0.7 joystick
 | 
			
		||||
| 
						 | 
				
			
			@ -3882,6 +3987,7 @@
 | 
			
		|||
1F9E9                                                  ; fully-qualified     # 🧩 E11.0 puzzle piece
 | 
			
		||||
1F9F8                                                  ; fully-qualified     # 🧸 E11.0 teddy bear
 | 
			
		||||
1FA85                                                  ; fully-qualified     # 🪅 E13.0 piñata
 | 
			
		||||
1FAA9                                                  ; fully-qualified     # 🪩 E14.0 mirror ball
 | 
			
		||||
1FA86                                                  ; fully-qualified     # 🪆 E13.0 nesting dolls
 | 
			
		||||
2660 FE0F                                              ; fully-qualified     # ♠️ E0.6 spade suit
 | 
			
		||||
2660                                                   ; unqualified         # ♠ E0.6 spade suit
 | 
			
		||||
| 
						 | 
				
			
			@ -3907,8 +4013,8 @@
 | 
			
		|||
1F9F6                                                  ; fully-qualified     # 🧶 E11.0 yarn
 | 
			
		||||
1FAA2                                                  ; fully-qualified     # 🪢 E13.0 knot
 | 
			
		||||
 | 
			
		||||
# Activities subtotal:		95
 | 
			
		||||
# Activities subtotal:		95	w/o modifiers
 | 
			
		||||
# Activities subtotal:		97
 | 
			
		||||
# Activities subtotal:		97	w/o modifiers
 | 
			
		||||
 | 
			
		||||
# group: Objects
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -4009,6 +4115,7 @@
 | 
			
		|||
 | 
			
		||||
# subgroup: computer
 | 
			
		||||
1F50B                                                  ; fully-qualified     # 🔋 E0.6 battery
 | 
			
		||||
1FAAB                                                  ; fully-qualified     # 🪫 E14.0 low battery
 | 
			
		||||
1F50C                                                  ; fully-qualified     # 🔌 E0.6 electric plug
 | 
			
		||||
1F4BB                                                  ; fully-qualified     # 💻 E0.6 laptop
 | 
			
		||||
1F5A5 FE0F                                             ; fully-qualified     # 🖥️ E0.7 desktop computer
 | 
			
		||||
| 
						 | 
				
			
			@ -4207,7 +4314,9 @@
 | 
			
		|||
1FA78                                                  ; fully-qualified     # 🩸 E12.0 drop of blood
 | 
			
		||||
1F48A                                                  ; fully-qualified     # 💊 E0.6 pill
 | 
			
		||||
1FA79                                                  ; fully-qualified     # 🩹 E12.0 adhesive bandage
 | 
			
		||||
1FA7C                                                  ; fully-qualified     # 🩼 E14.0 crutch
 | 
			
		||||
1FA7A                                                  ; fully-qualified     # 🩺 E12.0 stethoscope
 | 
			
		||||
1FA7B                                                  ; fully-qualified     # 🩻 E14.0 x-ray
 | 
			
		||||
 | 
			
		||||
# subgroup: household
 | 
			
		||||
1F6AA                                                  ; fully-qualified     # 🚪 E0.6 door
 | 
			
		||||
| 
						 | 
				
			
			@ -4232,6 +4341,7 @@
 | 
			
		|||
1F9FB                                                  ; fully-qualified     # 🧻 E11.0 roll of paper
 | 
			
		||||
1FAA3                                                  ; fully-qualified     # 🪣 E13.0 bucket
 | 
			
		||||
1F9FC                                                  ; fully-qualified     # 🧼 E11.0 soap
 | 
			
		||||
1FAE7                                                  ; fully-qualified     # 🫧 E14.0 bubbles
 | 
			
		||||
1FAA5                                                  ; fully-qualified     # 🪥 E13.0 toothbrush
 | 
			
		||||
1F9FD                                                  ; fully-qualified     # 🧽 E11.0 sponge
 | 
			
		||||
1F9EF                                                  ; fully-qualified     # 🧯 E11.0 fire extinguisher
 | 
			
		||||
| 
						 | 
				
			
			@ -4246,9 +4356,10 @@
 | 
			
		|||
26B1                                                   ; unqualified         # ⚱ E1.0 funeral urn
 | 
			
		||||
1F5FF                                                  ; fully-qualified     # 🗿 E0.6 moai
 | 
			
		||||
1FAA7                                                  ; fully-qualified     # 🪧 E13.0 placard
 | 
			
		||||
1FAAA                                                  ; fully-qualified     # 🪪 E14.0 identification card
 | 
			
		||||
 | 
			
		||||
# Objects subtotal:		299
 | 
			
		||||
# Objects subtotal:		299	w/o modifiers
 | 
			
		||||
# Objects subtotal:		304
 | 
			
		||||
# Objects subtotal:		304	w/o modifiers
 | 
			
		||||
 | 
			
		||||
# group: Symbols
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -4409,6 +4520,7 @@
 | 
			
		|||
2795                                                   ; fully-qualified     # ➕ E0.6 plus
 | 
			
		||||
2796                                                   ; fully-qualified     # ➖ E0.6 minus
 | 
			
		||||
2797                                                   ; fully-qualified     # ➗ E0.6 divide
 | 
			
		||||
1F7F0                                                  ; fully-qualified     # 🟰 E14.0 heavy equals sign
 | 
			
		||||
267E FE0F                                              ; fully-qualified     # ♾️ E11.0 infinity
 | 
			
		||||
267E                                                   ; unqualified         # ♾ E11.0 infinity
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -4581,8 +4693,8 @@
 | 
			
		|||
1F533                                                  ; fully-qualified     # 🔳 E0.6 white square button
 | 
			
		||||
1F532                                                  ; fully-qualified     # 🔲 E0.6 black square button
 | 
			
		||||
 | 
			
		||||
# Symbols subtotal:		301
 | 
			
		||||
# Symbols subtotal:		301	w/o modifiers
 | 
			
		||||
# Symbols subtotal:		302
 | 
			
		||||
# Symbols subtotal:		302	w/o modifiers
 | 
			
		||||
 | 
			
		||||
# group: Flags
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -4871,7 +4983,7 @@
 | 
			
		|||
# Flags subtotal:		275	w/o modifiers
 | 
			
		||||
 | 
			
		||||
# Status Counts
 | 
			
		||||
# fully-qualified : 3512
 | 
			
		||||
# fully-qualified : 3624
 | 
			
		||||
# minimally-qualified : 817
 | 
			
		||||
# unqualified : 252
 | 
			
		||||
# component : 9
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -61,7 +61,6 @@ def get_or_create_by_names(names) when is_list(names) do
 | 
			
		|||
               {:ok, Repo.all(from(ht in Hashtag, where: ht.name in ^names))}
 | 
			
		||||
             end)
 | 
			
		||||
             |> Repo.transaction() do
 | 
			
		||||
        Pleroma.Elasticsearch.maybe_bulk_post(hashtags, :hashtags)
 | 
			
		||||
        {:ok, hashtags}
 | 
			
		||||
      else
 | 
			
		||||
        {:error, _name, value, _changes_so_far} -> {:error, value}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -341,6 +341,14 @@ def destroy_multiple(%{id: user_id} = _user, ids) do
 | 
			
		|||
    |> Repo.delete_all()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def destroy_multiple_from_types(%{id: user_id}, types) do
 | 
			
		||||
    from(n in Notification,
 | 
			
		||||
      where: n.user_id == ^user_id,
 | 
			
		||||
      where: n.type in ^types
 | 
			
		||||
    )
 | 
			
		||||
    |> Repo.delete_all()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def dismiss(%Pleroma.Activity{} = activity) do
 | 
			
		||||
    Notification
 | 
			
		||||
    |> where([n], n.activity_id == ^activity.id)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,12 +1,17 @@
 | 
			
		|||
defmodule Pleroma.Search do
 | 
			
		||||
  @type search_map :: %{
 | 
			
		||||
          statuses: [map],
 | 
			
		||||
          accounts: [map],
 | 
			
		||||
          hashtags: [map]
 | 
			
		||||
        }
 | 
			
		||||
  alias Pleroma.Workers.SearchIndexingWorker
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Searches for stuff
 | 
			
		||||
  """
 | 
			
		||||
  @callback search(map, map, keyword) :: search_map
 | 
			
		||||
  def add_to_index(%Pleroma.Activity{id: activity_id}) do
 | 
			
		||||
    SearchIndexingWorker.enqueue("add_to_index", %{"activity" => activity_id})
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def remove_from_index(%Pleroma.Object{id: object_id}) do
 | 
			
		||||
    SearchIndexingWorker.enqueue("remove_from_index", %{"object" => object_id})
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def search(query, options) do
 | 
			
		||||
    search_module = Pleroma.Config.get([Pleroma.Search, :module], Pleroma.Activity)
 | 
			
		||||
 | 
			
		||||
    search_module.search(options[:for_user], query, options)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,138 +0,0 @@
 | 
			
		|||
defmodule Pleroma.Search.Builtin do
 | 
			
		||||
  @behaviour Pleroma.Search
 | 
			
		||||
 | 
			
		||||
  alias Pleroma.Repo
 | 
			
		||||
  alias Pleroma.User
 | 
			
		||||
  alias Pleroma.Activity
 | 
			
		||||
  alias Pleroma.Web.MastodonAPI.AccountView
 | 
			
		||||
  alias Pleroma.Web.MastodonAPI.StatusView
 | 
			
		||||
  alias Pleroma.Web.Endpoint
 | 
			
		||||
 | 
			
		||||
  require Logger
 | 
			
		||||
 | 
			
		||||
  @impl Pleroma.Search
 | 
			
		||||
  def search(_conn, %{q: query} = params, options) do
 | 
			
		||||
    version = Keyword.get(options, :version)
 | 
			
		||||
    timeout = Keyword.get(Repo.config(), :timeout, 15_000)
 | 
			
		||||
    query = String.trim(query)
 | 
			
		||||
    default_values = %{"statuses" => [], "accounts" => [], "hashtags" => []}
 | 
			
		||||
 | 
			
		||||
    default_values
 | 
			
		||||
    |> Enum.map(fn {resource, default_value} ->
 | 
			
		||||
      if params[:type] in [nil, resource] do
 | 
			
		||||
        {resource, fn -> resource_search(version, resource, query, options) end}
 | 
			
		||||
      else
 | 
			
		||||
        {resource, fn -> default_value end}
 | 
			
		||||
      end
 | 
			
		||||
    end)
 | 
			
		||||
    |> Task.async_stream(fn {resource, f} -> {resource, with_fallback(f)} end,
 | 
			
		||||
      timeout: timeout,
 | 
			
		||||
      on_timeout: :kill_task
 | 
			
		||||
    )
 | 
			
		||||
    |> Enum.reduce(default_values, fn
 | 
			
		||||
      {:ok, {resource, result}}, acc ->
 | 
			
		||||
        Map.put(acc, resource, result)
 | 
			
		||||
 | 
			
		||||
      _error, acc ->
 | 
			
		||||
        acc
 | 
			
		||||
    end)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp resource_search(_, "accounts", query, options) do
 | 
			
		||||
    accounts = with_fallback(fn -> User.search(query, options) end)
 | 
			
		||||
 | 
			
		||||
    AccountView.render("index.json",
 | 
			
		||||
      users: accounts,
 | 
			
		||||
      for: options[:for_user],
 | 
			
		||||
      embed_relationships: options[:embed_relationships]
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp resource_search(_, "statuses", query, options) do
 | 
			
		||||
    statuses = with_fallback(fn -> Activity.search(options[:for_user], query, options) end)
 | 
			
		||||
 | 
			
		||||
    StatusView.render("index.json",
 | 
			
		||||
      activities: statuses,
 | 
			
		||||
      for: options[:for_user],
 | 
			
		||||
      as: :activity
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp resource_search(:v2, "hashtags", query, options) do
 | 
			
		||||
    tags_path = Endpoint.url() <> "/tag/"
 | 
			
		||||
 | 
			
		||||
    query
 | 
			
		||||
    |> prepare_tags(options)
 | 
			
		||||
    |> Enum.map(fn tag ->
 | 
			
		||||
      %{name: tag, url: tags_path <> tag}
 | 
			
		||||
    end)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp resource_search(:v1, "hashtags", query, options) do
 | 
			
		||||
    prepare_tags(query, options)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp prepare_tags(query, options) do
 | 
			
		||||
    tags =
 | 
			
		||||
      query
 | 
			
		||||
      |> preprocess_uri_query()
 | 
			
		||||
      |> String.split(~r/[^#\w]+/u, trim: true)
 | 
			
		||||
      |> Enum.uniq_by(&String.downcase/1)
 | 
			
		||||
 | 
			
		||||
    explicit_tags = Enum.filter(tags, fn tag -> String.starts_with?(tag, "#") end)
 | 
			
		||||
 | 
			
		||||
    tags =
 | 
			
		||||
      if Enum.any?(explicit_tags) do
 | 
			
		||||
        explicit_tags
 | 
			
		||||
      else
 | 
			
		||||
        tags
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    tags = Enum.map(tags, fn tag -> String.trim_leading(tag, "#") end)
 | 
			
		||||
 | 
			
		||||
    tags =
 | 
			
		||||
      if Enum.empty?(explicit_tags) && !options[:skip_joined_tag] do
 | 
			
		||||
        add_joined_tag(tags)
 | 
			
		||||
      else
 | 
			
		||||
        tags
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    Pleroma.Pagination.paginate(tags, options)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # If `query` is a URI, returns last component of its path, otherwise returns `query`
 | 
			
		||||
  defp preprocess_uri_query(query) do
 | 
			
		||||
    if query =~ ~r/https?:\/\// do
 | 
			
		||||
      query
 | 
			
		||||
      |> String.trim_trailing("/")
 | 
			
		||||
      |> URI.parse()
 | 
			
		||||
      |> Map.get(:path)
 | 
			
		||||
      |> String.split("/")
 | 
			
		||||
      |> Enum.at(-1)
 | 
			
		||||
    else
 | 
			
		||||
      query
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp add_joined_tag(tags) do
 | 
			
		||||
    tags
 | 
			
		||||
    |> Kernel.++([joined_tag(tags)])
 | 
			
		||||
    |> Enum.uniq_by(&String.downcase/1)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp joined_tag(tags) do
 | 
			
		||||
    tags
 | 
			
		||||
    |> Enum.map(fn tag -> String.capitalize(tag) end)
 | 
			
		||||
    |> Enum.join()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp with_fallback(f, fallback \\ []) do
 | 
			
		||||
    try do
 | 
			
		||||
      f.()
 | 
			
		||||
    rescue
 | 
			
		||||
      error ->
 | 
			
		||||
        Logger.error("#{__MODULE__} search error: #{inspect(error)}")
 | 
			
		||||
        fallback
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -2,7 +2,7 @@
 | 
			
		|||
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
 | 
			
		||||
# SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
defmodule Pleroma.Activity.Search do
 | 
			
		||||
defmodule Pleroma.Search.DatabaseSearch do
 | 
			
		||||
  alias Pleroma.Activity
 | 
			
		||||
  alias Pleroma.Object.Fetcher
 | 
			
		||||
  alias Pleroma.Pagination
 | 
			
		||||
| 
						 | 
				
			
			@ -13,6 +13,8 @@ defmodule Pleroma.Activity.Search do
 | 
			
		|||
 | 
			
		||||
  import Ecto.Query
 | 
			
		||||
 | 
			
		||||
  @behaviour Pleroma.Search.SearchBackend
 | 
			
		||||
 | 
			
		||||
  def search(user, search_query, options \\ []) do
 | 
			
		||||
    index_type = if Pleroma.Config.get([:database, :rum_enabled]), do: :rum, else: :gin
 | 
			
		||||
    limit = Enum.min([Keyword.get(options, :limit), 40])
 | 
			
		||||
| 
						 | 
				
			
			@ -45,6 +47,12 @@ def search(user, search_query, options \\ []) do
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def add_to_index(_activity), do: nil
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def remove_from_index(_object), do: nil
 | 
			
		||||
 | 
			
		||||
  def maybe_restrict_author(query, %User{} = author) do
 | 
			
		||||
    Activity.Queries.by_author(query, author)
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			@ -57,7 +65,7 @@ def maybe_restrict_blocked(query, %User{} = user) do
 | 
			
		|||
 | 
			
		||||
  def maybe_restrict_blocked(query, _), do: query
 | 
			
		||||
 | 
			
		||||
  defp restrict_public(q) do
 | 
			
		||||
  def restrict_public(q) do
 | 
			
		||||
    from([a, o] in q,
 | 
			
		||||
      where: fragment("?->>'type' = 'Create'", a.data),
 | 
			
		||||
      where: ^Pleroma.Constants.as_public() in a.recipients
 | 
			
		||||
| 
						 | 
				
			
			@ -124,7 +132,7 @@ defp query_with(q, :rum, search_query, :websearch) do
 | 
			
		|||
    )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp maybe_restrict_local(q, user) do
 | 
			
		||||
  def maybe_restrict_local(q, user) do
 | 
			
		||||
    limit = Pleroma.Config.get([:instance, :limit_to_local_content], :unauthenticated)
 | 
			
		||||
 | 
			
		||||
    case {limit, user} do
 | 
			
		||||
| 
						 | 
				
			
			@ -137,7 +145,7 @@ defp maybe_restrict_local(q, user) do
 | 
			
		|||
 | 
			
		||||
  defp restrict_local(q), do: where(q, local: true)
 | 
			
		||||
 | 
			
		||||
  defp maybe_fetch(activities, user, search_query) do
 | 
			
		||||
  def maybe_fetch(activities, user, search_query) do
 | 
			
		||||
    with true <- Regex.match?(~r/https?:/, search_query),
 | 
			
		||||
         {:ok, object} <- Fetcher.fetch_object_from_id(search_query),
 | 
			
		||||
         %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
 | 
			
		||||
| 
						 | 
				
			
			@ -3,24 +3,22 @@
 | 
			
		|||
# SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
defmodule Pleroma.Search.Elasticsearch do
 | 
			
		||||
  @behaviour Pleroma.Search
 | 
			
		||||
  @behaviour Pleroma.Search.SearchBackend
 | 
			
		||||
 | 
			
		||||
  alias Pleroma.Activity
 | 
			
		||||
  alias Pleroma.Object.Fetcher
 | 
			
		||||
  alias Pleroma.Web.MastodonAPI.StatusView
 | 
			
		||||
  alias Pleroma.Web.MastodonAPI.AccountView
 | 
			
		||||
  alias Pleroma.Web.ActivityPub.Visibility
 | 
			
		||||
  alias Pleroma.Search.Elasticsearch.Parsers
 | 
			
		||||
  alias Pleroma.Web.Endpoint
 | 
			
		||||
 | 
			
		||||
  def es_query(:activity, query) do
 | 
			
		||||
  def es_query(:activity, query, offset, limit) do
 | 
			
		||||
    must = Parsers.Activity.parse(query)
 | 
			
		||||
 | 
			
		||||
    if must == [] do
 | 
			
		||||
      :skip
 | 
			
		||||
    else
 | 
			
		||||
      %{
 | 
			
		||||
        size: 50,
 | 
			
		||||
        size: limit,
 | 
			
		||||
        from: offset,
 | 
			
		||||
        terminate_after: 50,
 | 
			
		||||
        timeout: "5s",
 | 
			
		||||
        sort: [
 | 
			
		||||
| 
						 | 
				
			
			@ -36,50 +34,6 @@ def es_query(:activity, query) do
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def es_query(:user, query) do
 | 
			
		||||
    must = Parsers.User.parse(query)
 | 
			
		||||
 | 
			
		||||
    if must == [] do
 | 
			
		||||
      :skip
 | 
			
		||||
    else
 | 
			
		||||
      %{
 | 
			
		||||
        size: 50,
 | 
			
		||||
        terminate_after: 50,
 | 
			
		||||
        timeout: "5s",
 | 
			
		||||
        sort: [
 | 
			
		||||
          "_score"
 | 
			
		||||
        ],
 | 
			
		||||
        query: %{
 | 
			
		||||
          bool: %{
 | 
			
		||||
            must: must
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def es_query(:hashtag, query) do
 | 
			
		||||
    must = Parsers.Hashtag.parse(query)
 | 
			
		||||
 | 
			
		||||
    if must == [] do
 | 
			
		||||
      :skip
 | 
			
		||||
    else
 | 
			
		||||
      %{
 | 
			
		||||
        size: 50,
 | 
			
		||||
        terminate_after: 50,
 | 
			
		||||
        timeout: "5s",
 | 
			
		||||
        sort: [
 | 
			
		||||
          "_score"
 | 
			
		||||
        ],
 | 
			
		||||
        query: %{
 | 
			
		||||
          bool: %{
 | 
			
		||||
            must: Parsers.Hashtag.parse(query)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp maybe_fetch(:activity, search_query) do
 | 
			
		||||
    with true <- Regex.match?(~r/https?:/, search_query),
 | 
			
		||||
         {:ok, object} <- Fetcher.fetch_object_from_id(search_query),
 | 
			
		||||
| 
						 | 
				
			
			@ -90,8 +44,10 @@ defp maybe_fetch(:activity, search_query) do
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @impl Pleroma.Search
 | 
			
		||||
  def search(%{assigns: %{user: user}} = _conn, %{q: query} = _params, _options) do
 | 
			
		||||
  def search(user, query, options) do
 | 
			
		||||
    limit = Enum.min([Keyword.get(options, :limit), 40])
 | 
			
		||||
    offset = Keyword.get(options, :offset, 0)
 | 
			
		||||
 | 
			
		||||
    parsed_query =
 | 
			
		||||
      query
 | 
			
		||||
      |> String.trim()
 | 
			
		||||
| 
						 | 
				
			
			@ -104,30 +60,13 @@ def search(%{assigns: %{user: user}} = _conn, %{q: query} = _params, _options) d
 | 
			
		|||
 | 
			
		||||
    activity_task =
 | 
			
		||||
      Task.async(fn ->
 | 
			
		||||
        q = es_query(:activity, parsed_query)
 | 
			
		||||
        q = es_query(:activity, parsed_query, offset, limit)
 | 
			
		||||
 | 
			
		||||
        Pleroma.Elasticsearch.search(:activities, q)
 | 
			
		||||
        Pleroma.Search.Elasticsearch.Store.search(:activities, q)
 | 
			
		||||
        |> Enum.filter(fn x -> Visibility.visible_for_user?(x, user) end)
 | 
			
		||||
      end)
 | 
			
		||||
 | 
			
		||||
    user_task =
 | 
			
		||||
      Task.async(fn ->
 | 
			
		||||
        q = es_query(:user, parsed_query)
 | 
			
		||||
 | 
			
		||||
        Pleroma.Elasticsearch.search(:users, q)
 | 
			
		||||
        |> Enum.filter(fn x -> Pleroma.User.visible_for(x, user) == :visible end)
 | 
			
		||||
      end)
 | 
			
		||||
 | 
			
		||||
    hashtag_task =
 | 
			
		||||
      Task.async(fn ->
 | 
			
		||||
        q = es_query(:hashtag, parsed_query)
 | 
			
		||||
 | 
			
		||||
        Pleroma.Elasticsearch.search(:hashtags, q)
 | 
			
		||||
      end)
 | 
			
		||||
 | 
			
		||||
    activity_results = Task.await(activity_task)
 | 
			
		||||
    user_results = Task.await(user_task)
 | 
			
		||||
    hashtag_results = Task.await(hashtag_task)
 | 
			
		||||
    direct_activity = Task.await(activity_fetch_task)
 | 
			
		||||
 | 
			
		||||
    activity_results =
 | 
			
		||||
| 
						 | 
				
			
			@ -137,25 +76,16 @@ def search(%{assigns: %{user: user}} = _conn, %{q: query} = _params, _options) d
 | 
			
		|||
        [direct_activity | activity_results]
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    %{
 | 
			
		||||
      "accounts" =>
 | 
			
		||||
        AccountView.render("index.json",
 | 
			
		||||
          users: user_results,
 | 
			
		||||
          for: user
 | 
			
		||||
        ),
 | 
			
		||||
      "hashtags" =>
 | 
			
		||||
        Enum.map(hashtag_results, fn x ->
 | 
			
		||||
          %{
 | 
			
		||||
            url: Endpoint.url() <> "/tag/" <> x,
 | 
			
		||||
            name: x
 | 
			
		||||
          }
 | 
			
		||||
        end),
 | 
			
		||||
      "statuses" =>
 | 
			
		||||
        StatusView.render("index.json",
 | 
			
		||||
          activities: activity_results,
 | 
			
		||||
          for: user,
 | 
			
		||||
          as: :activity
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
    activity_results
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def add_to_index(activity) do
 | 
			
		||||
    Elasticsearch.put_document(Pleroma.Search.Elasticsearch.Cluster, activity, "activities")
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def remove_from_index(object) do
 | 
			
		||||
    Elasticsearch.delete_document(Pleroma.Search.Elasticsearch.Cluster, object, "activities")
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										4
									
								
								lib/pleroma/search/elasticsearch/cluster.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								lib/pleroma/search/elasticsearch/cluster.ex
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,4 @@
 | 
			
		|||
defmodule Pleroma.Search.Elasticsearch.Cluster do
 | 
			
		||||
  @moduledoc false
 | 
			
		||||
  use Elasticsearch.Cluster, otp_app: :pleroma
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,61 @@
 | 
			
		|||
# Akkoma: A lightweight social networking server
 | 
			
		||||
# Copyright © 2022-2022 Akkoma Authors <https://git.ihatebeinga.live/IHBAGang/akkoma/>
 | 
			
		||||
# SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
defimpl Elasticsearch.Document, for: Pleroma.Activity do
 | 
			
		||||
  alias Pleroma.Object
 | 
			
		||||
  require Pleroma.Constants
 | 
			
		||||
 | 
			
		||||
  def id(obj), do: obj.id
 | 
			
		||||
  def routing(_), do: false
 | 
			
		||||
 | 
			
		||||
  def object_to_search_data(object) do
 | 
			
		||||
    # Only index public or unlisted Notes
 | 
			
		||||
    if not is_nil(object) and object.data["type"] == "Note" and
 | 
			
		||||
         not is_nil(object.data["content"]) and
 | 
			
		||||
         (Pleroma.Constants.as_public() in object.data["to"] or
 | 
			
		||||
            Pleroma.Constants.as_public() in object.data["cc"]) and
 | 
			
		||||
         String.length(object.data["content"]) > 1 do
 | 
			
		||||
      data = object.data
 | 
			
		||||
 | 
			
		||||
      content_str =
 | 
			
		||||
        case data["content"] do
 | 
			
		||||
          [nil | rest] -> to_string(rest)
 | 
			
		||||
          str -> str
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
      content =
 | 
			
		||||
        with {:ok, scrubbed} <- FastSanitize.strip_tags(content_str),
 | 
			
		||||
             trimmed <- String.trim(scrubbed) do
 | 
			
		||||
          trimmed
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
      if String.length(content) > 1 do
 | 
			
		||||
        {:ok, published, _} = DateTime.from_iso8601(data["published"])
 | 
			
		||||
 | 
			
		||||
        %{
 | 
			
		||||
          _timestamp: published,
 | 
			
		||||
          content: content,
 | 
			
		||||
          instance: URI.parse(object.data["actor"]).host,
 | 
			
		||||
          hashtags: Object.hashtags(object),
 | 
			
		||||
          user: Pleroma.User.get_cached_by_ap_id(object.data["actor"]).nickname
 | 
			
		||||
        }
 | 
			
		||||
      else
 | 
			
		||||
        %{}
 | 
			
		||||
      end
 | 
			
		||||
    else
 | 
			
		||||
      %{}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def encode(activity) do
 | 
			
		||||
    object = Pleroma.Object.normalize(activity)
 | 
			
		||||
    object_to_search_data(object)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
defimpl Elasticsearch.Document, for: Pleroma.Object do
 | 
			
		||||
  def id(obj), do: obj.id
 | 
			
		||||
  def routing(_), do: false
 | 
			
		||||
  def encode(_), do: nil
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -1,34 +0,0 @@
 | 
			
		|||
# Akkoma: A lightweight social networking server
 | 
			
		||||
# Copyright © 2022-2022 Akkoma Authors <https://git.ihatebeinga.live/IHBAGang/akkoma/>
 | 
			
		||||
# SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
defmodule Pleroma.Search.Elasticsearch.Parsers.Hashtag do
 | 
			
		||||
  defp to_es(term) when is_binary(term) do
 | 
			
		||||
    %{
 | 
			
		||||
      term: %{
 | 
			
		||||
        hashtag: %{
 | 
			
		||||
          value: String.downcase(term)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp to_es({:quoted, term}), do: to_es(term)
 | 
			
		||||
 | 
			
		||||
  defp to_es({:filter, ["hashtag", query]}) do
 | 
			
		||||
    %{
 | 
			
		||||
      term: %{
 | 
			
		||||
        hashtag: %{
 | 
			
		||||
          value: String.downcase(query)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp to_es({:filter, _}), do: nil
 | 
			
		||||
 | 
			
		||||
  def parse(q) do
 | 
			
		||||
    Enum.map(q, &to_es/1)
 | 
			
		||||
    |> Enum.filter(fn x -> x != nil end)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										52
									
								
								lib/pleroma/search/elasticsearch/store.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								lib/pleroma/search/elasticsearch/store.ex
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,52 @@
 | 
			
		|||
# Akkoma: A lightweight social networking server
 | 
			
		||||
# Copyright © 2022-2022 Akkoma Authors <https://git.ihatebeinga.live/IHBAGang/akkoma/>
 | 
			
		||||
# SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
defmodule Pleroma.Search.Elasticsearch.Store do
 | 
			
		||||
  @behaviour Elasticsearch.Store
 | 
			
		||||
  alias Pleroma.Search.Elasticsearch.Cluster
 | 
			
		||||
  require Logger
 | 
			
		||||
 | 
			
		||||
  alias Pleroma.Repo
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def stream(schema) do
 | 
			
		||||
    Repo.stream(schema)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def transaction(fun) do
 | 
			
		||||
    {:ok, result} = Repo.transaction(fun, timeout: :infinity)
 | 
			
		||||
    result
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def search(_, _, _, :skip), do: []
 | 
			
		||||
 | 
			
		||||
  def search(:raw, index, q) do
 | 
			
		||||
    with {:ok, raw_results} <- Elasticsearch.post(Cluster, "/#{index}/_search", q) do
 | 
			
		||||
      results =
 | 
			
		||||
        raw_results
 | 
			
		||||
        |> Map.get("hits", %{})
 | 
			
		||||
        |> Map.get("hits", [])
 | 
			
		||||
 | 
			
		||||
      {:ok, results}
 | 
			
		||||
    else
 | 
			
		||||
      {:error, e} ->
 | 
			
		||||
        Logger.error(e)
 | 
			
		||||
        {:error, e}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def search(:activities, q) do
 | 
			
		||||
    with {:ok, results} <- search(:raw, "activities", q) do
 | 
			
		||||
      results
 | 
			
		||||
      |> Enum.map(fn result -> result["_id"] end)
 | 
			
		||||
      |> Pleroma.Activity.all_by_ids_with_object()
 | 
			
		||||
      |> Enum.sort(&(&1.inserted_at >= &2.inserted_at))
 | 
			
		||||
    else
 | 
			
		||||
      e ->
 | 
			
		||||
        Logger.error(e)
 | 
			
		||||
        []
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -1,57 +0,0 @@
 | 
			
		|||
# Akkoma: A lightweight social networking server
 | 
			
		||||
# Copyright © 2022-2022 Akkoma Authors <https://git.ihatebeinga.live/IHBAGang/akkoma/>
 | 
			
		||||
# SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
defmodule Pleroma.Search.Elasticsearch.Parsers.User do
 | 
			
		||||
  defp to_es(term) when is_binary(term) do
 | 
			
		||||
    %{
 | 
			
		||||
      bool: %{
 | 
			
		||||
        minimum_should_match: 1,
 | 
			
		||||
        should: [
 | 
			
		||||
          %{
 | 
			
		||||
            match: %{
 | 
			
		||||
              bio: %{
 | 
			
		||||
                query: term,
 | 
			
		||||
                operator: "AND"
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          %{
 | 
			
		||||
            term: %{
 | 
			
		||||
              nickname: %{
 | 
			
		||||
                value: term
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          %{
 | 
			
		||||
            match: %{
 | 
			
		||||
              display_name: %{
 | 
			
		||||
                query: term,
 | 
			
		||||
                operator: "AND"
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        ]
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp to_es({:quoted, term}), do: to_es(term)
 | 
			
		||||
 | 
			
		||||
  defp to_es({:filter, ["user", query]}) do
 | 
			
		||||
    %{
 | 
			
		||||
      term: %{
 | 
			
		||||
        nickname: %{
 | 
			
		||||
          value: query
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp to_es({:filter, _}), do: nil
 | 
			
		||||
 | 
			
		||||
  def parse(q) do
 | 
			
		||||
    Enum.map(q, &to_es/1)
 | 
			
		||||
    |> Enum.filter(fn x -> x != nil end)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										169
									
								
								lib/pleroma/search/meilisearch.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								lib/pleroma/search/meilisearch.ex
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,169 @@
 | 
			
		|||
defmodule Pleroma.Search.Meilisearch do
 | 
			
		||||
  require Logger
 | 
			
		||||
  require Pleroma.Constants
 | 
			
		||||
 | 
			
		||||
  alias Pleroma.Activity
 | 
			
		||||
 | 
			
		||||
  import Pleroma.Search.DatabaseSearch
 | 
			
		||||
  import Ecto.Query
 | 
			
		||||
 | 
			
		||||
  @behaviour Pleroma.Search.SearchBackend
 | 
			
		||||
 | 
			
		||||
  defp meili_headers do
 | 
			
		||||
    private_key = Pleroma.Config.get([Pleroma.Search.Meilisearch, :private_key])
 | 
			
		||||
 | 
			
		||||
    [{"Content-Type", "application/json"}] ++
 | 
			
		||||
      if is_nil(private_key), do: [], else: [{"Authorization", "Bearer #{private_key}"}]
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def meili_get(path) do
 | 
			
		||||
    endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url])
 | 
			
		||||
 | 
			
		||||
    result =
 | 
			
		||||
      Pleroma.HTTP.get(
 | 
			
		||||
        Path.join(endpoint, path),
 | 
			
		||||
        meili_headers()
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
    with {:ok, res} <- result do
 | 
			
		||||
      {:ok, Jason.decode!(res.body)}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def meili_post(path, params) do
 | 
			
		||||
    endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url])
 | 
			
		||||
 | 
			
		||||
    result =
 | 
			
		||||
      Pleroma.HTTP.post(
 | 
			
		||||
        Path.join(endpoint, path),
 | 
			
		||||
        Jason.encode!(params),
 | 
			
		||||
        meili_headers()
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
    with {:ok, res} <- result do
 | 
			
		||||
      {:ok, Jason.decode!(res.body)}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def meili_put(path, params) do
 | 
			
		||||
    endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url])
 | 
			
		||||
 | 
			
		||||
    result =
 | 
			
		||||
      Pleroma.HTTP.request(
 | 
			
		||||
        :put,
 | 
			
		||||
        Path.join(endpoint, path),
 | 
			
		||||
        Jason.encode!(params),
 | 
			
		||||
        meili_headers(),
 | 
			
		||||
        []
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
    with {:ok, res} <- result do
 | 
			
		||||
      {:ok, Jason.decode!(res.body)}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def meili_delete!(path) do
 | 
			
		||||
    endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url])
 | 
			
		||||
 | 
			
		||||
    {:ok, _} =
 | 
			
		||||
      Pleroma.HTTP.request(
 | 
			
		||||
        :delete,
 | 
			
		||||
        Path.join(endpoint, path),
 | 
			
		||||
        "",
 | 
			
		||||
        meili_headers(),
 | 
			
		||||
        []
 | 
			
		||||
      )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def search(user, query, options \\ []) do
 | 
			
		||||
    limit = Enum.min([Keyword.get(options, :limit), 40])
 | 
			
		||||
    offset = Keyword.get(options, :offset, 0)
 | 
			
		||||
    author = Keyword.get(options, :author)
 | 
			
		||||
 | 
			
		||||
    res =
 | 
			
		||||
      meili_post(
 | 
			
		||||
        "/indexes/objects/search",
 | 
			
		||||
        %{q: query, offset: offset, limit: limit}
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
    with {:ok, result} <- res do
 | 
			
		||||
      hits = result["hits"] |> Enum.map(& &1["ap"])
 | 
			
		||||
 | 
			
		||||
      try do
 | 
			
		||||
        hits
 | 
			
		||||
        |> Activity.create_by_object_ap_id()
 | 
			
		||||
        |> Activity.with_preloaded_object()
 | 
			
		||||
        |> Activity.with_preloaded_object()
 | 
			
		||||
        |> Activity.restrict_deactivated_users()
 | 
			
		||||
        |> maybe_restrict_local(user)
 | 
			
		||||
        |> maybe_restrict_author(author)
 | 
			
		||||
        |> maybe_restrict_blocked(user)
 | 
			
		||||
        |> maybe_fetch(user, query)
 | 
			
		||||
        |> order_by([object: obj], desc: obj.data["published"])
 | 
			
		||||
        |> Pleroma.Repo.all()
 | 
			
		||||
      rescue
 | 
			
		||||
        _ -> maybe_fetch([], user, query)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def object_to_search_data(object) do
 | 
			
		||||
    # Only index public or unlisted Notes
 | 
			
		||||
    if not is_nil(object) and object.data["type"] == "Note" and
 | 
			
		||||
         not is_nil(object.data["content"]) and
 | 
			
		||||
         (Pleroma.Constants.as_public() in object.data["to"] or
 | 
			
		||||
            Pleroma.Constants.as_public() in object.data["cc"]) and
 | 
			
		||||
         String.length(object.data["content"]) > 1 do
 | 
			
		||||
      data = object.data
 | 
			
		||||
 | 
			
		||||
      content_str =
 | 
			
		||||
        case data["content"] do
 | 
			
		||||
          [nil | rest] -> to_string(rest)
 | 
			
		||||
          str -> str
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
      content =
 | 
			
		||||
        with {:ok, scrubbed} <- FastSanitize.strip_tags(content_str),
 | 
			
		||||
             trimmed <- String.trim(scrubbed) do
 | 
			
		||||
          trimmed
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
      if String.length(content) > 1 do
 | 
			
		||||
        {:ok, published, _} = DateTime.from_iso8601(data["published"])
 | 
			
		||||
 | 
			
		||||
        %{
 | 
			
		||||
          id: object.id,
 | 
			
		||||
          content: content,
 | 
			
		||||
          ap: data["id"],
 | 
			
		||||
          published: published |> DateTime.to_unix()
 | 
			
		||||
        }
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def add_to_index(activity) do
 | 
			
		||||
    maybe_search_data = object_to_search_data(activity.object)
 | 
			
		||||
 | 
			
		||||
    if activity.data["type"] == "Create" and maybe_search_data do
 | 
			
		||||
      result =
 | 
			
		||||
        meili_put(
 | 
			
		||||
          "/indexes/objects/documents",
 | 
			
		||||
          [maybe_search_data]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
      with {:ok, res} <- result,
 | 
			
		||||
           true <- Map.has_key?(res, "uid") do
 | 
			
		||||
        # Do nothing
 | 
			
		||||
      else
 | 
			
		||||
        _ ->
 | 
			
		||||
          Logger.error("Failed to add activity #{activity.id} to index: #{inspect(result)}")
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def remove_from_index(object) do
 | 
			
		||||
    meili_delete!("/indexes/objects/documents/#{object.id}")
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										17
									
								
								lib/pleroma/search/search_backend.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								lib/pleroma/search/search_backend.ex
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,17 @@
 | 
			
		|||
defmodule Pleroma.Search.SearchBackend do
 | 
			
		||||
  @doc """
 | 
			
		||||
  Add the object associated with the activity to the search index.
 | 
			
		||||
 | 
			
		||||
  The whole activity is passed, to allow filtering on things such as scope.
 | 
			
		||||
  """
 | 
			
		||||
  @callback add_to_index(activity :: Pleroma.Activity.t()) :: nil
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Remove the object from the index.
 | 
			
		||||
 | 
			
		||||
  Just the object, as opposed to the whole activity, is passed, since the object
 | 
			
		||||
  is what contains the actual content and there is no need for fitlering when removing
 | 
			
		||||
  from index.
 | 
			
		||||
  """
 | 
			
		||||
  @callback remove_from_index(object :: Pleroma.Object.t()) :: nil
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -12,8 +12,7 @@ defmodule Pleroma.Telemetry.Logger do
 | 
			
		|||
    [:pleroma, :connection_pool, :reclaim, :stop],
 | 
			
		||||
    [:pleroma, :connection_pool, :provision_failure],
 | 
			
		||||
    [:pleroma, :connection_pool, :client, :dead],
 | 
			
		||||
    [:pleroma, :connection_pool, :client, :add],
 | 
			
		||||
    [:pleroma, :repo, :query]
 | 
			
		||||
    [:pleroma, :connection_pool, :client, :add]
 | 
			
		||||
  ]
 | 
			
		||||
  def attach do
 | 
			
		||||
    :telemetry.attach_many(
 | 
			
		||||
| 
						 | 
				
			
			@ -93,64 +92,4 @@ def handle_event(
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_event([:pleroma, :connection_pool, :client, :add], _, _, _), do: :ok
 | 
			
		||||
 | 
			
		||||
  def handle_event(
 | 
			
		||||
        [:pleroma, :repo, :query] = _name,
 | 
			
		||||
        %{query_time: query_time} = measurements,
 | 
			
		||||
        %{source: source} = metadata,
 | 
			
		||||
        config
 | 
			
		||||
      ) do
 | 
			
		||||
    logging_config = Pleroma.Config.get([:telemetry, :slow_queries_logging], [])
 | 
			
		||||
 | 
			
		||||
    if logging_config[:enabled] &&
 | 
			
		||||
         logging_config[:min_duration] &&
 | 
			
		||||
         query_time > logging_config[:min_duration] and
 | 
			
		||||
         (is_nil(logging_config[:exclude_sources]) or
 | 
			
		||||
            source not in logging_config[:exclude_sources]) do
 | 
			
		||||
      log_slow_query(measurements, metadata, config)
 | 
			
		||||
    else
 | 
			
		||||
      :ok
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp log_slow_query(
 | 
			
		||||
         %{query_time: query_time} = _measurements,
 | 
			
		||||
         %{source: _source, query: query, params: query_params, repo: repo} = _metadata,
 | 
			
		||||
         _config
 | 
			
		||||
       ) do
 | 
			
		||||
    sql_explain =
 | 
			
		||||
      with {:ok, %{rows: explain_result_rows}} <-
 | 
			
		||||
             repo.query("EXPLAIN " <> query, query_params, log: false) do
 | 
			
		||||
        Enum.map_join(explain_result_rows, "\n", & &1)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    {:current_stacktrace, stacktrace} = Process.info(self(), :current_stacktrace)
 | 
			
		||||
 | 
			
		||||
    pleroma_stacktrace =
 | 
			
		||||
      Enum.filter(stacktrace, fn
 | 
			
		||||
        {__MODULE__, _, _, _} ->
 | 
			
		||||
          false
 | 
			
		||||
 | 
			
		||||
        {mod, _, _, _} ->
 | 
			
		||||
          mod
 | 
			
		||||
          |> to_string()
 | 
			
		||||
          |> String.starts_with?("Elixir.Pleroma.")
 | 
			
		||||
      end)
 | 
			
		||||
 | 
			
		||||
    Logger.warn(fn ->
 | 
			
		||||
      """
 | 
			
		||||
      Slow query!
 | 
			
		||||
 | 
			
		||||
      Total time: #{round(query_time / 1_000)} ms
 | 
			
		||||
 | 
			
		||||
      #{query}
 | 
			
		||||
 | 
			
		||||
      #{inspect(query_params, limit: :infinity)}
 | 
			
		||||
 | 
			
		||||
      #{sql_explain}
 | 
			
		||||
 | 
			
		||||
      #{Exception.format_stacktrace(pleroma_stacktrace)}
 | 
			
		||||
      """
 | 
			
		||||
    end)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -151,6 +151,7 @@ defmodule Pleroma.User do
 | 
			
		|||
    field(:pinned_objects, :map, default: %{})
 | 
			
		||||
    field(:is_suggested, :boolean, default: false)
 | 
			
		||||
    field(:last_status_at, :naive_datetime)
 | 
			
		||||
    field(:language, :string)
 | 
			
		||||
 | 
			
		||||
    embeds_one(
 | 
			
		||||
      :notification_settings,
 | 
			
		||||
| 
						 | 
				
			
			@ -734,7 +735,8 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do
 | 
			
		|||
      :password_confirmation,
 | 
			
		||||
      :emoji,
 | 
			
		||||
      :accepts_chat_messages,
 | 
			
		||||
      :registration_reason
 | 
			
		||||
      :registration_reason,
 | 
			
		||||
      :language
 | 
			
		||||
    ])
 | 
			
		||||
    |> validate_required([:name, :nickname, :password, :password_confirmation])
 | 
			
		||||
    |> validate_confirmation(:password)
 | 
			
		||||
| 
						 | 
				
			
			@ -1089,11 +1091,24 @@ def update_and_set_cache(struct, params) do
 | 
			
		|||
    |> update_and_set_cache()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def update_and_set_cache(changeset) do
 | 
			
		||||
  def update_and_set_cache(%{data: %Pleroma.User{} = user} = changeset) do
 | 
			
		||||
    was_superuser_before_update = User.superuser?(user)
 | 
			
		||||
 | 
			
		||||
    with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
 | 
			
		||||
      Pleroma.Elasticsearch.maybe_put_into_elasticsearch(user)
 | 
			
		||||
      set_cache(user)
 | 
			
		||||
    end
 | 
			
		||||
    |> maybe_remove_report_notifications(was_superuser_before_update)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp maybe_remove_report_notifications({:ok, %Pleroma.User{} = user} = result, true) do
 | 
			
		||||
    if not User.superuser?(user),
 | 
			
		||||
      do: user |> Notification.destroy_multiple_from_types(["pleroma:report"])
 | 
			
		||||
 | 
			
		||||
    result
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp maybe_remove_report_notifications(result, _) do
 | 
			
		||||
    result
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def get_user_friends_ap_ids(user) do
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -140,6 +140,9 @@ def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when
 | 
			
		|||
        Task.start(fn -> Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) end)
 | 
			
		||||
      end)
 | 
			
		||||
 | 
			
		||||
      # Add local posts to search index
 | 
			
		||||
      if local, do: Pleroma.Search.add_to_index(activity)
 | 
			
		||||
 | 
			
		||||
      {:ok, activity}
 | 
			
		||||
    else
 | 
			
		||||
      %Activity{} = activity ->
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,7 +24,7 @@ defp score_displayname("federationbot"), do: 1.0
 | 
			
		|||
  defp score_displayname("fedibot"), do: 1.0
 | 
			
		||||
  defp score_displayname(_), do: 0.0
 | 
			
		||||
 | 
			
		||||
  defp determine_if_followbot(%User{nickname: nickname, name: displayname}) do
 | 
			
		||||
  defp determine_if_followbot(%User{nickname: nickname, name: displayname, actor_type: actor_type}) do
 | 
			
		||||
    # nickname will be a binary string except when following a relay
 | 
			
		||||
    nick_score =
 | 
			
		||||
      if is_binary(nickname) do
 | 
			
		||||
| 
						 | 
				
			
			@ -45,19 +45,32 @@ defp determine_if_followbot(%User{nickname: nickname, name: displayname}) do
 | 
			
		|||
        0.0
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    nick_score + name_score
 | 
			
		||||
    # actor_type "Service" is a Bot account
 | 
			
		||||
    actor_type_score =
 | 
			
		||||
      if actor_type == "Service" do
 | 
			
		||||
        1.0
 | 
			
		||||
      else
 | 
			
		||||
        0.0
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    nick_score + name_score + actor_type_score
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp determine_if_followbot(_), do: 0.0
 | 
			
		||||
 | 
			
		||||
  defp bot_allowed?(%{"object" => target}, bot_actor) do
 | 
			
		||||
    %User{} = user = normalize_by_ap_id(target)
 | 
			
		||||
 | 
			
		||||
    User.following?(user, bot_actor)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def filter(%{"type" => "Follow", "actor" => actor_id} = message) do
 | 
			
		||||
    %User{} = actor = normalize_by_ap_id(actor_id)
 | 
			
		||||
 | 
			
		||||
    score = determine_if_followbot(actor)
 | 
			
		||||
 | 
			
		||||
    # TODO: scan biography data for keywords and score it somehow.
 | 
			
		||||
    if score < 0.8 do
 | 
			
		||||
    if score < 0.8 || bot_allowed?(message, actor) do
 | 
			
		||||
      {:ok, message}
 | 
			
		||||
    else
 | 
			
		||||
      {:reject, "[AntiFollowbotPolicy] Scored #{actor_id} as #{score}"}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,6 +12,14 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
 | 
			
		|||
 | 
			
		||||
  defp accept_host?(host), do: host in Config.get([:mrf_steal_emoji, :hosts], [])
 | 
			
		||||
 | 
			
		||||
  defp shortcode_matches?(shortcode, pattern) when is_binary(pattern) do
 | 
			
		||||
    shortcode == pattern
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp shortcode_matches?(shortcode, pattern) do
 | 
			
		||||
    String.match?(shortcode, pattern)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp steal_emoji({shortcode, url}, emoji_dir_path) do
 | 
			
		||||
    url = Pleroma.Web.MediaProxy.url(url)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -72,7 +80,7 @@ def filter(%{"object" => %{"emoji" => foreign_emojis, "actor" => actor}} = messa
 | 
			
		|||
          reject_emoji? =
 | 
			
		||||
            [:mrf_steal_emoji, :rejected_shortcodes]
 | 
			
		||||
            |> Config.get([])
 | 
			
		||||
            |> Enum.find(false, fn regex -> String.match?(shortcode, regex) end)
 | 
			
		||||
            |> Enum.find(false, fn pattern -> shortcode_matches?(shortcode, pattern) end)
 | 
			
		||||
 | 
			
		||||
          !reject_emoji?
 | 
			
		||||
        end)
 | 
			
		||||
| 
						 | 
				
			
			@ -122,8 +130,12 @@ def config_description do
 | 
			
		|||
        %{
 | 
			
		||||
          key: :rejected_shortcodes,
 | 
			
		||||
          type: {:list, :string},
 | 
			
		||||
          description: "Regex-list of shortcodes to reject",
 | 
			
		||||
          suggestions: [""]
 | 
			
		||||
          description: """
 | 
			
		||||
            A list of patterns or matches to reject shortcodes with.
 | 
			
		||||
 | 
			
		||||
            Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
 | 
			
		||||
          """,
 | 
			
		||||
          suggestions: ["foo", ~r/foo/]
 | 
			
		||||
        },
 | 
			
		||||
        %{
 | 
			
		||||
          key: :size_limit,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -108,6 +108,7 @@ defp fix(data) do
 | 
			
		|||
    |> fix_replies()
 | 
			
		||||
    |> fix_source()
 | 
			
		||||
    |> fix_misskey_content()
 | 
			
		||||
    |> Transmogrifier.fix_attachments()
 | 
			
		||||
    |> Transmogrifier.fix_emoji()
 | 
			
		||||
    |> Transmogrifier.fix_content_map()
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -28,7 +28,6 @@ def common_pipeline(object, meta) do
 | 
			
		|||
    case Repo.transaction(fn -> do_common_pipeline(object, meta) end, Utils.query_timeout()) do
 | 
			
		||||
      {:ok, {:ok, activity, meta}} ->
 | 
			
		||||
        side_effects().handle_after_transaction(meta)
 | 
			
		||||
        side_effects().handle_after_transaction(activity)
 | 
			
		||||
        {:ok, activity, meta}
 | 
			
		||||
 | 
			
		||||
      {:ok, value} ->
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
# Pleroma: A lightweight social networking server
 | 
			
		||||
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
 | 
			
		||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
 | 
			
		||||
# SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
defmodule Pleroma.Web.ActivityPub.SideEffects do
 | 
			
		||||
| 
						 | 
				
			
			@ -193,6 +193,7 @@ def handle(%{data: %{"type" => "Like"}} = object, meta) do
 | 
			
		|||
  # - Increase replies count
 | 
			
		||||
  # - Set up ActivityExpiration
 | 
			
		||||
  # - Set up notifications
 | 
			
		||||
  # - Index incoming posts for search (if needed)
 | 
			
		||||
  @impl true
 | 
			
		||||
  def handle(%{data: %{"type" => "Create"}} = activity, meta) do
 | 
			
		||||
    with {:ok, object, meta} <- handle_object_creation(meta[:object_data], activity, meta),
 | 
			
		||||
| 
						 | 
				
			
			@ -222,6 +223,8 @@ def handle(%{data: %{"type" => "Create"}} = activity, meta) do
 | 
			
		|||
        Task.start(fn -> Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) end)
 | 
			
		||||
      end)
 | 
			
		||||
 | 
			
		||||
      Pleroma.Search.add_to_index(Map.put(activity, :object, object))
 | 
			
		||||
 | 
			
		||||
      meta =
 | 
			
		||||
        meta
 | 
			
		||||
        |> add_notifications(notifications)
 | 
			
		||||
| 
						 | 
				
			
			@ -269,6 +272,7 @@ def handle(%{data: %{"type" => "Undo", "object" => undone_object}} = object, met
 | 
			
		|||
  def handle(%{data: %{"type" => "EmojiReact"}} = object, meta) do
 | 
			
		||||
    reacted_object = Object.get_by_ap_id(object.data["object"])
 | 
			
		||||
    Utils.add_emoji_reaction_to_object(object, reacted_object)
 | 
			
		||||
 | 
			
		||||
    Notification.create_notifications(object)
 | 
			
		||||
 | 
			
		||||
    {:ok, object, meta}
 | 
			
		||||
| 
						 | 
				
			
			@ -281,6 +285,7 @@ def handle(%{data: %{"type" => "EmojiReact"}} = object, meta) do
 | 
			
		|||
  # - Reduce the user note count
 | 
			
		||||
  # - Reduce the reply count
 | 
			
		||||
  # - Stream out the activity
 | 
			
		||||
  # - Removes posts from search index (if needed)
 | 
			
		||||
  @impl true
 | 
			
		||||
  def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object, meta) do
 | 
			
		||||
    deleted_object =
 | 
			
		||||
| 
						 | 
				
			
			@ -320,6 +325,12 @@ def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object,
 | 
			
		|||
 | 
			
		||||
    if result == :ok do
 | 
			
		||||
      Notification.create_notifications(object)
 | 
			
		||||
 | 
			
		||||
      # Only remove from index when deleting actual objects, not users or anything else
 | 
			
		||||
      with %Pleroma.Object{} <- deleted_object do
 | 
			
		||||
        Pleroma.Search.remove_from_index(deleted_object)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      {:ok, object, meta}
 | 
			
		||||
    else
 | 
			
		||||
      {:error, result}
 | 
			
		||||
| 
						 | 
				
			
			@ -537,24 +548,6 @@ defp add_notifications(meta, notifications) do
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def handle_after_transaction(%Pleroma.Activity{data: %{"type" => "Create"}} = activity) do
 | 
			
		||||
    Pleroma.Elasticsearch.put_by_id(:activity, activity.id)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_after_transaction(%Pleroma.Activity{
 | 
			
		||||
        data: %{"type" => "Delete", "deleted_activity_id" => id}
 | 
			
		||||
      }) do
 | 
			
		||||
    Pleroma.Elasticsearch.delete_by_id(:activity, id)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_after_transaction(%Pleroma.Activity{}) do
 | 
			
		||||
    :ok
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_after_transaction(%Pleroma.Object{}) do
 | 
			
		||||
    :ok
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_after_transaction(meta) do
 | 
			
		||||
    meta
 | 
			
		||||
    |> send_notifications()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
# Pleroma: A lightweight social networking server
 | 
			
		||||
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
 | 
			
		||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
 | 
			
		||||
# SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
defmodule Pleroma.Web.ActivityPub.SideEffects.Handling do
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -507,6 +507,11 @@ defp create_request do
 | 
			
		|||
          type: :string,
 | 
			
		||||
          nullable: true,
 | 
			
		||||
          description: "Invite token required when the registrations aren't public"
 | 
			
		||||
        },
 | 
			
		||||
        language: %Schema{
 | 
			
		||||
          type: :string,
 | 
			
		||||
          nullable: true,
 | 
			
		||||
          description: "User's preferred language for emails"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      example: %{
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -396,13 +396,7 @@ def listen(user, data) do
 | 
			
		|||
 | 
			
		||||
  def post(user, %{status: _} = data) do
 | 
			
		||||
    with {:ok, draft} <- ActivityDraft.create(user, data) do
 | 
			
		||||
      activity = ActivityPub.create(draft.changes, draft.preview?)
 | 
			
		||||
 | 
			
		||||
      unless draft.preview? do
 | 
			
		||||
        Pleroma.Elasticsearch.maybe_put_into_elasticsearch(activity)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      activity
 | 
			
		||||
      ActivityPub.create(draft.changes, draft.preview?)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,6 +9,7 @@ defmodule Pleroma.Web.Feed.FeedView do
 | 
			
		|||
  alias Pleroma.Formatter
 | 
			
		||||
  alias Pleroma.Object
 | 
			
		||||
  alias Pleroma.User
 | 
			
		||||
  alias Pleroma.Web.Gettext
 | 
			
		||||
  alias Pleroma.Web.MediaProxy
 | 
			
		||||
 | 
			
		||||
  require Pleroma.Constants
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,4 +25,196 @@ defmodule Pleroma.Web.Gettext do
 | 
			
		|||
  See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
 | 
			
		||||
  """
 | 
			
		||||
  use Gettext, otp_app: :pleroma
 | 
			
		||||
 | 
			
		||||
  def language_tag do
 | 
			
		||||
    # Naive implementation: HTML lang attribute uses BCP 47, which
 | 
			
		||||
    # uses - as a separator.
 | 
			
		||||
    # https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang
 | 
			
		||||
 | 
			
		||||
    Gettext.get_locale()
 | 
			
		||||
    |> String.replace("_", "-", global: true)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def normalize_locale(locale) do
 | 
			
		||||
    if is_binary(locale) do
 | 
			
		||||
      String.replace(locale, "-", "_", global: true)
 | 
			
		||||
    else
 | 
			
		||||
      nil
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def supports_locale?(locale) do
 | 
			
		||||
    Pleroma.Web.Gettext
 | 
			
		||||
    |> Gettext.known_locales()
 | 
			
		||||
    |> Enum.member?(locale)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def variant?(locale), do: String.contains?(locale, "_")
 | 
			
		||||
 | 
			
		||||
  def language_for_variant(locale) do
 | 
			
		||||
    Enum.at(String.split(locale, "_"), 0)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def ensure_fallbacks(locales) do
 | 
			
		||||
    locales
 | 
			
		||||
    |> Enum.flat_map(fn locale ->
 | 
			
		||||
      others =
 | 
			
		||||
        other_supported_variants_of_locale(locale)
 | 
			
		||||
        |> Enum.filter(fn l -> not Enum.member?(locales, l) end)
 | 
			
		||||
 | 
			
		||||
      [locale] ++ others
 | 
			
		||||
    end)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def other_supported_variants_of_locale(locale) do
 | 
			
		||||
    cond do
 | 
			
		||||
      supports_locale?(locale) ->
 | 
			
		||||
        []
 | 
			
		||||
 | 
			
		||||
      variant?(locale) ->
 | 
			
		||||
        lang = language_for_variant(locale)
 | 
			
		||||
        if supports_locale?(lang), do: [lang], else: []
 | 
			
		||||
 | 
			
		||||
      true ->
 | 
			
		||||
        Gettext.known_locales(Pleroma.Web.Gettext)
 | 
			
		||||
        |> Enum.filter(fn l -> String.starts_with?(l, locale <> "_") end)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def get_locales do
 | 
			
		||||
    Process.get({Pleroma.Web.Gettext, :locales}, [])
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def is_locale_list(locales) do
 | 
			
		||||
    Enum.all?(locales, &is_binary/1)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def put_locales(locales) do
 | 
			
		||||
    if is_locale_list(locales) do
 | 
			
		||||
      Process.put({Pleroma.Web.Gettext, :locales}, Enum.uniq(locales))
 | 
			
		||||
      Gettext.put_locale(Enum.at(locales, 0, Gettext.get_locale()))
 | 
			
		||||
      :ok
 | 
			
		||||
    else
 | 
			
		||||
      {:error, :not_locale_list}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def locale_or_default(locale) do
 | 
			
		||||
    if supports_locale?(locale) do
 | 
			
		||||
      locale
 | 
			
		||||
    else
 | 
			
		||||
      Gettext.get_locale()
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def with_locales_func(locales, fun) do
 | 
			
		||||
    prev_locales = Process.get({Pleroma.Web.Gettext, :locales})
 | 
			
		||||
    put_locales(locales)
 | 
			
		||||
 | 
			
		||||
    try do
 | 
			
		||||
      fun.()
 | 
			
		||||
    after
 | 
			
		||||
      if prev_locales do
 | 
			
		||||
        put_locales(prev_locales)
 | 
			
		||||
      else
 | 
			
		||||
        Process.delete({Pleroma.Web.Gettext, :locales})
 | 
			
		||||
        Process.delete(Gettext)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defmacro with_locales(locales, do: fun) do
 | 
			
		||||
    quote do
 | 
			
		||||
      Pleroma.Web.Gettext.with_locales_func(unquote(locales), fn ->
 | 
			
		||||
        unquote(fun)
 | 
			
		||||
      end)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def to_locale_list(locale) when is_binary(locale) do
 | 
			
		||||
    locale
 | 
			
		||||
    |> String.split(",")
 | 
			
		||||
    |> Enum.filter(&supports_locale?/1)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def to_locale_list(_), do: []
 | 
			
		||||
 | 
			
		||||
  defmacro with_locale_or_default(locale, do: fun) do
 | 
			
		||||
    quote do
 | 
			
		||||
      Pleroma.Web.Gettext.with_locales_func(
 | 
			
		||||
        Pleroma.Web.Gettext.to_locale_list(unquote(locale))
 | 
			
		||||
        |> Enum.concat(Pleroma.Web.Gettext.get_locales()),
 | 
			
		||||
        fn ->
 | 
			
		||||
          unquote(fun)
 | 
			
		||||
        end
 | 
			
		||||
      )
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp next_locale(locale, list) do
 | 
			
		||||
    index = Enum.find_index(list, fn item -> item == locale end)
 | 
			
		||||
 | 
			
		||||
    if not is_nil(index) do
 | 
			
		||||
      Enum.at(list, index + 1)
 | 
			
		||||
    else
 | 
			
		||||
      nil
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # We do not yet have a proper English translation. The "English"
 | 
			
		||||
  # version is currently but the fallback msgid. However, this
 | 
			
		||||
  # will not work if the user puts English as the first language,
 | 
			
		||||
  # and at the same time specifies other languages, as gettext will
 | 
			
		||||
  # think the English translation is missing, and call
 | 
			
		||||
  # handle_missing_translation functions. This may result in
 | 
			
		||||
  # text in other languages being shown even if English is preferred
 | 
			
		||||
  # by the user.
 | 
			
		||||
  #
 | 
			
		||||
  # To prevent this, we do not allow fallbacking when the current
 | 
			
		||||
  # locale missing a translation is English.
 | 
			
		||||
  defp should_fallback?(locale) do
 | 
			
		||||
    locale != "en"
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_missing_translation(locale, domain, msgctxt, msgid, bindings) do
 | 
			
		||||
    next = next_locale(locale, get_locales())
 | 
			
		||||
 | 
			
		||||
    if is_nil(next) or not should_fallback?(locale) do
 | 
			
		||||
      super(locale, domain, msgctxt, msgid, bindings)
 | 
			
		||||
    else
 | 
			
		||||
      {:ok,
 | 
			
		||||
       Gettext.with_locale(next, fn ->
 | 
			
		||||
         Gettext.dpgettext(Pleroma.Web.Gettext, domain, msgctxt, msgid, bindings)
 | 
			
		||||
       end)}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_missing_plural_translation(
 | 
			
		||||
        locale,
 | 
			
		||||
        domain,
 | 
			
		||||
        msgctxt,
 | 
			
		||||
        msgid,
 | 
			
		||||
        msgid_plural,
 | 
			
		||||
        n,
 | 
			
		||||
        bindings
 | 
			
		||||
      ) do
 | 
			
		||||
    next = next_locale(locale, get_locales())
 | 
			
		||||
 | 
			
		||||
    if is_nil(next) or not should_fallback?(locale) do
 | 
			
		||||
      super(locale, domain, msgctxt, msgid, msgid_plural, n, bindings)
 | 
			
		||||
    else
 | 
			
		||||
      {:ok,
 | 
			
		||||
       Gettext.with_locale(next, fn ->
 | 
			
		||||
         Gettext.dpngettext(
 | 
			
		||||
           Pleroma.Web.Gettext,
 | 
			
		||||
           domain,
 | 
			
		||||
           msgctxt,
 | 
			
		||||
           msgid,
 | 
			
		||||
           msgid_plural,
 | 
			
		||||
           n,
 | 
			
		||||
           bindings
 | 
			
		||||
         )
 | 
			
		||||
       end)}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -217,6 +217,7 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p
 | 
			
		|||
      |> Maps.put_if_present(:is_locked, params[:locked])
 | 
			
		||||
      # Note: param name is indeed :discoverable (not an error)
 | 
			
		||||
      |> Maps.put_if_present(:is_discoverable, params[:discoverable])
 | 
			
		||||
      |> Maps.put_if_present(:language, Pleroma.Web.Gettext.normalize_locale(params[:language]))
 | 
			
		||||
 | 
			
		||||
    # What happens here:
 | 
			
		||||
    #
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,13 +1,16 @@
 | 
			
		|||
# Pleroma: A lightweight social networking server
 | 
			
		||||
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
 | 
			
		||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
 | 
			
		||||
# SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
defmodule Pleroma.Web.MastodonAPI.SearchController do
 | 
			
		||||
  use Pleroma.Web, :controller
 | 
			
		||||
 | 
			
		||||
  alias Pleroma.Repo
 | 
			
		||||
  alias Pleroma.User
 | 
			
		||||
  alias Pleroma.Web.ControllerHelper
 | 
			
		||||
  alias Pleroma.Web.Endpoint
 | 
			
		||||
  alias Pleroma.Web.MastodonAPI.AccountView
 | 
			
		||||
  alias Pleroma.Web.MastodonAPI.StatusView
 | 
			
		||||
  alias Pleroma.Web.Plugs.OAuthScopesPlug
 | 
			
		||||
  alias Pleroma.Web.Plugs.RateLimiter
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -41,13 +44,34 @@ def account_search(%{assigns: %{user: user}} = conn, %{q: query} = params) do
 | 
			
		|||
  def search2(conn, params), do: do_search(:v2, conn, params)
 | 
			
		||||
  def search(conn, params), do: do_search(:v1, conn, params)
 | 
			
		||||
 | 
			
		||||
  defp do_search(version, %{assigns: %{user: user}} = conn, params) do
 | 
			
		||||
    options =
 | 
			
		||||
      search_options(params, user)
 | 
			
		||||
      |> Keyword.put(:version, version)
 | 
			
		||||
  defp do_search(version, %{assigns: %{user: user}} = conn, %{q: query} = params) do
 | 
			
		||||
    query = String.trim(query)
 | 
			
		||||
    options = search_options(params, user)
 | 
			
		||||
    timeout = Keyword.get(Repo.config(), :timeout, 15_000)
 | 
			
		||||
    default_values = %{"statuses" => [], "accounts" => [], "hashtags" => []}
 | 
			
		||||
 | 
			
		||||
    search_provider = Pleroma.Config.get([:search, :provider])
 | 
			
		||||
    json(conn, search_provider.search(conn, params, options))
 | 
			
		||||
    result =
 | 
			
		||||
      default_values
 | 
			
		||||
      |> Enum.map(fn {resource, default_value} ->
 | 
			
		||||
        if params[:type] in [nil, resource] do
 | 
			
		||||
          {resource, fn -> resource_search(version, resource, query, options) end}
 | 
			
		||||
        else
 | 
			
		||||
          {resource, fn -> default_value end}
 | 
			
		||||
        end
 | 
			
		||||
      end)
 | 
			
		||||
      |> Task.async_stream(fn {resource, f} -> {resource, with_fallback(f)} end,
 | 
			
		||||
        timeout: timeout,
 | 
			
		||||
        on_timeout: :kill_task
 | 
			
		||||
      )
 | 
			
		||||
      |> Enum.reduce(default_values, fn
 | 
			
		||||
        {:ok, {resource, result}}, acc ->
 | 
			
		||||
          Map.put(acc, resource, result)
 | 
			
		||||
 | 
			
		||||
        _error, acc ->
 | 
			
		||||
          acc
 | 
			
		||||
      end)
 | 
			
		||||
 | 
			
		||||
    json(conn, result)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp search_options(params, user) do
 | 
			
		||||
| 
						 | 
				
			
			@ -64,6 +88,104 @@ defp search_options(params, user) do
 | 
			
		|||
    |> Enum.filter(&elem(&1, 1))
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp resource_search(_, "accounts", query, options) do
 | 
			
		||||
    accounts = with_fallback(fn -> User.search(query, options) end)
 | 
			
		||||
 | 
			
		||||
    AccountView.render("index.json",
 | 
			
		||||
      users: accounts,
 | 
			
		||||
      for: options[:for_user],
 | 
			
		||||
      embed_relationships: options[:embed_relationships]
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp resource_search(_, "statuses", query, options) do
 | 
			
		||||
    statuses = with_fallback(fn -> Pleroma.Search.search(query, options) end)
 | 
			
		||||
 | 
			
		||||
    StatusView.render("index.json",
 | 
			
		||||
      activities: statuses,
 | 
			
		||||
      for: options[:for_user],
 | 
			
		||||
      as: :activity
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp resource_search(:v2, "hashtags", query, options) do
 | 
			
		||||
    tags_path = Endpoint.url() <> "/tag/"
 | 
			
		||||
 | 
			
		||||
    query
 | 
			
		||||
    |> prepare_tags(options)
 | 
			
		||||
    |> Enum.map(fn tag ->
 | 
			
		||||
      %{name: tag, url: tags_path <> tag}
 | 
			
		||||
    end)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp resource_search(:v1, "hashtags", query, options) do
 | 
			
		||||
    prepare_tags(query, options)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp prepare_tags(query, options) do
 | 
			
		||||
    tags =
 | 
			
		||||
      query
 | 
			
		||||
      |> preprocess_uri_query()
 | 
			
		||||
      |> String.split(~r/[^#\w]+/u, trim: true)
 | 
			
		||||
      |> Enum.uniq_by(&String.downcase/1)
 | 
			
		||||
 | 
			
		||||
    explicit_tags = Enum.filter(tags, fn tag -> String.starts_with?(tag, "#") end)
 | 
			
		||||
 | 
			
		||||
    tags =
 | 
			
		||||
      if Enum.any?(explicit_tags) do
 | 
			
		||||
        explicit_tags
 | 
			
		||||
      else
 | 
			
		||||
        tags
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    tags = Enum.map(tags, fn tag -> String.trim_leading(tag, "#") end)
 | 
			
		||||
 | 
			
		||||
    tags =
 | 
			
		||||
      if Enum.empty?(explicit_tags) && !options[:skip_joined_tag] do
 | 
			
		||||
        add_joined_tag(tags)
 | 
			
		||||
      else
 | 
			
		||||
        tags
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    Pleroma.Pagination.paginate(tags, options)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp add_joined_tag(tags) do
 | 
			
		||||
    tags
 | 
			
		||||
    |> Kernel.++([joined_tag(tags)])
 | 
			
		||||
    |> Enum.uniq_by(&String.downcase/1)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # If `query` is a URI, returns last component of its path, otherwise returns `query`
 | 
			
		||||
  defp preprocess_uri_query(query) do
 | 
			
		||||
    if query =~ ~r/https?:\/\// do
 | 
			
		||||
      query
 | 
			
		||||
      |> String.trim_trailing("/")
 | 
			
		||||
      |> URI.parse()
 | 
			
		||||
      |> Map.get(:path)
 | 
			
		||||
      |> String.split("/")
 | 
			
		||||
      |> Enum.at(-1)
 | 
			
		||||
    else
 | 
			
		||||
      query
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp joined_tag(tags) do
 | 
			
		||||
    tags
 | 
			
		||||
    |> Enum.map(fn tag -> String.capitalize(tag) end)
 | 
			
		||||
    |> Enum.join()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp with_fallback(f, fallback \\ []) do
 | 
			
		||||
    try do
 | 
			
		||||
      f.()
 | 
			
		||||
    rescue
 | 
			
		||||
      error ->
 | 
			
		||||
        Logger.error("#{__MODULE__} search error: #{inspect(error)}")
 | 
			
		||||
        fallback
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp get_author(%{account_id: account_id}) when is_binary(account_id),
 | 
			
		||||
    do: User.get_cached_by_id(account_id)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -384,11 +384,13 @@ def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
 | 
			
		|||
  def context(%{assigns: %{user: user}} = conn, %{id: id}) do
 | 
			
		||||
    with %Activity{} = activity <- Activity.get_by_id(id) do
 | 
			
		||||
      activities =
 | 
			
		||||
        ActivityPub.fetch_activities_for_context(activity.data["context"], %{
 | 
			
		||||
        activity.data["context"]
 | 
			
		||||
        |> ActivityPub.fetch_activities_for_context(%{
 | 
			
		||||
          blocking_user: user,
 | 
			
		||||
          user: user,
 | 
			
		||||
          exclude_id: activity.id
 | 
			
		||||
        })
 | 
			
		||||
        |> Enum.filter(fn activity -> Visibility.visible_for_user?(activity, user) end)
 | 
			
		||||
 | 
			
		||||
      render(conn, "context.json", activity: activity, activities: activities, user: user)
 | 
			
		||||
    end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,7 @@ defmodule Pleroma.Web.OAuth.MFAView do
 | 
			
		|||
  use Pleroma.Web, :view
 | 
			
		||||
  import Phoenix.HTML.Form
 | 
			
		||||
  alias Pleroma.MFA
 | 
			
		||||
  alias Pleroma.Web.Gettext
 | 
			
		||||
 | 
			
		||||
  def render("mfa_response.json", %{token: token, user: user}) do
 | 
			
		||||
    %{
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,6 +5,8 @@
 | 
			
		|||
defmodule Pleroma.Web.OAuth.OAuthView do
 | 
			
		||||
  use Pleroma.Web, :view
 | 
			
		||||
  import Phoenix.HTML.Form
 | 
			
		||||
  import Phoenix.HTML
 | 
			
		||||
  alias Pleroma.Web.Gettext
 | 
			
		||||
 | 
			
		||||
  alias Pleroma.Web.OAuth.Token.Utils
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,18 +6,56 @@
 | 
			
		|||
defmodule Pleroma.Web.Plugs.SetLocalePlug do
 | 
			
		||||
  import Plug.Conn, only: [get_req_header: 2, assign: 3]
 | 
			
		||||
 | 
			
		||||
  def frontend_language_cookie_name, do: "userLanguage"
 | 
			
		||||
 | 
			
		||||
  def init(_), do: nil
 | 
			
		||||
 | 
			
		||||
  def call(conn, _) do
 | 
			
		||||
    locale = get_locale_from_header(conn) || Gettext.get_locale()
 | 
			
		||||
    Gettext.put_locale(locale)
 | 
			
		||||
    assign(conn, :locale, locale)
 | 
			
		||||
    locales = get_locales_from_header(conn)
 | 
			
		||||
    first_locale = Enum.at(locales, 0, Gettext.get_locale())
 | 
			
		||||
 | 
			
		||||
    Pleroma.Web.Gettext.put_locales(locales)
 | 
			
		||||
 | 
			
		||||
    conn
 | 
			
		||||
    |> assign(:locale, first_locale)
 | 
			
		||||
    |> assign(:locales, locales)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp get_locale_from_header(conn) do
 | 
			
		||||
  defp get_locales_from_header(conn) do
 | 
			
		||||
    conn
 | 
			
		||||
    |> extract_accept_language()
 | 
			
		||||
    |> Enum.find(&supported_locale?/1)
 | 
			
		||||
    |> extract_preferred_language()
 | 
			
		||||
    |> normalize_language_codes()
 | 
			
		||||
    |> all_supported()
 | 
			
		||||
    |> Enum.uniq()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp all_supported(locales) do
 | 
			
		||||
    locales
 | 
			
		||||
    |> Pleroma.Web.Gettext.ensure_fallbacks()
 | 
			
		||||
    |> Enum.filter(&supported_locale?/1)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp normalize_language_codes(codes) do
 | 
			
		||||
    codes
 | 
			
		||||
    |> Enum.map(fn code -> Pleroma.Web.Gettext.normalize_locale(code) end)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp extract_preferred_language(conn) do
 | 
			
		||||
    extract_frontend_language(conn) ++ extract_accept_language(conn)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp extract_frontend_language(conn) do
 | 
			
		||||
    %{req_cookies: cookies} =
 | 
			
		||||
      conn
 | 
			
		||||
      |> Plug.Conn.fetch_cookies()
 | 
			
		||||
 | 
			
		||||
    case cookies[frontend_language_cookie_name()] do
 | 
			
		||||
      nil ->
 | 
			
		||||
        []
 | 
			
		||||
 | 
			
		||||
      fe_lang ->
 | 
			
		||||
        String.split(fe_lang, ",")
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp extract_accept_language(conn) do
 | 
			
		||||
| 
						 | 
				
			
			@ -29,7 +67,6 @@ defp extract_accept_language(conn) do
 | 
			
		|||
        |> Enum.sort(&(&1.quality > &2.quality))
 | 
			
		||||
        |> Enum.map(& &1.tag)
 | 
			
		||||
        |> Enum.reject(&is_nil/1)
 | 
			
		||||
        |> ensure_language_fallbacks()
 | 
			
		||||
 | 
			
		||||
      _ ->
 | 
			
		||||
        []
 | 
			
		||||
| 
						 | 
				
			
			@ -37,9 +74,7 @@ defp extract_accept_language(conn) do
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  defp supported_locale?(locale) do
 | 
			
		||||
    Pleroma.Web.Gettext
 | 
			
		||||
    |> Gettext.known_locales()
 | 
			
		||||
    |> Enum.member?(locale)
 | 
			
		||||
    Pleroma.Web.Gettext.supports_locale?(locale)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp parse_language_option(string) do
 | 
			
		||||
| 
						 | 
				
			
			@ -53,11 +88,4 @@ defp parse_language_option(string) do
 | 
			
		|||
 | 
			
		||||
    %{tag: captures["tag"], quality: quality}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp ensure_language_fallbacks(tags) do
 | 
			
		||||
    Enum.flat_map(tags, fn tag ->
 | 
			
		||||
      [language | _] = String.split(tag, "-")
 | 
			
		||||
      if Enum.member?(tags, language), do: [tag], else: [tag, language]
 | 
			
		||||
    end)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -160,7 +160,7 @@
 | 
			
		|||
												<div
 | 
			
		||||
													style="font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif;line-height: 14px; color: <%= @styling.header_color %>;">
 | 
			
		||||
													<p style="line-height: 36px; text-align: center; margin: 0;"><span
 | 
			
		||||
															style="font-size: 30px; color: <%= @styling.header_color %>;">Hey <%= @user.nickname %>, here is what you've missed!</span></p>
 | 
			
		||||
															style="font-size: 30px; color: <%= @styling.header_color %>;"><%= Gettext.dpgettext("static_pages", "digest email header line", "Hey %{nickname}, here is what you've missed!", nickname: @user.nickname) %></span></p>
 | 
			
		||||
												</div>
 | 
			
		||||
											</div>
 | 
			
		||||
											<!--[if mso]></td></tr></table><![endif]-->
 | 
			
		||||
| 
						 | 
				
			
			@ -382,7 +382,7 @@
 | 
			
		|||
												<div
 | 
			
		||||
													style="font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-size: 12px; line-height: 14px; color: <%= @styling.text_color %>;">
 | 
			
		||||
													<p style="font-size: 12px; line-height: 24px; text-align: center; margin: 0;"><span
 | 
			
		||||
															style="font-size: 20px;"><%= length(@followers) %> New Followers</span><span
 | 
			
		||||
															style="font-size: 20px;"><%= Gettext.dpngettext("static_pages", "new followers count header", "%{count} New Follower", "%{count} New Followers", length(@followers), count: length(@followers)) %></span><span
 | 
			
		||||
															style="font-size: 20px; line-height: 24px;"></span></p>
 | 
			
		||||
												</div>
 | 
			
		||||
											</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -535,16 +535,16 @@
 | 
			
		|||
												style="color:<%= @styling.text_color %>;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;line-height:120%;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
 | 
			
		||||
												<p
 | 
			
		||||
													style="font-size: 12px; line-height: 16px; text-align: center; color: <%= @styling.text_color %>; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; margin: 0;">
 | 
			
		||||
													<span style="font-size: 14px;">You have received this email because you have signed up to receive digest emails from <b><%= @instance %></b> Pleroma instance.</span></p>
 | 
			
		||||
													<span style="font-size: 14px;"><%= raw Gettext.dpgettext("static_pages", "digest email sending reason", "You have received this email because you have signed up to receive digest emails from <b>%{instance}</b> Pleroma instance.", instance: safe_to_string(html_escape(@instance))) %></span></p>
 | 
			
		||||
												<p
 | 
			
		||||
													style="font-size: 12px; line-height: 14px; text-align: center; color: <%= @styling.text_color %>; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; margin: 0;">
 | 
			
		||||
													 </p>
 | 
			
		||||
												<p
 | 
			
		||||
													style="font-size: 12px; line-height: 16px; text-align: center; color: <%= @styling.text_color %>; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; margin: 0;">
 | 
			
		||||
													<span style="font-size: 14px;">The email address you are subscribed as is <a href="mailto:<%= @user.email %>" style="color: <%= @styling.link_color %>;text-decoration: none;"><%= @user.email %></a>. </span></p>
 | 
			
		||||
													<span style="font-size: 14px;"><%= raw Gettext.dpgettext("static_pages", "digest email receiver address", "The email address you are subscribed as is <a href='mailto:%{@user.email}' style='color: %{color};text-decoration: none;'>%{email}</a>. ", color: safe_to_string(html_escape(@styling.link_color)), email: safe_to_string(html_escape(@user.email))) %></span></p>
 | 
			
		||||
												<p
 | 
			
		||||
													style="font-size: 12px; line-height: 16px; text-align: center; color: <%= @styling.text_color %>; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; margin: 0;">
 | 
			
		||||
													<span style="font-size: 14px;">To unsubscribe, please go <%= link "here", style: "color: #{@styling.link_color};text-decoration: none;", to: @unsubscribe_link %>.</span></p>
 | 
			
		||||
													<span style="font-size: 14px;"><%= raw Gettext.dpgettext("static_pages", "digest email unsubscribe action", "To unsubscribe, please go %{here}.", here: safe_to_string link(Gettext.dpgettext("static_pages", "digest email unsubscribe action link text", "here"), style: "color: #{@styling.link_color};text-decoration: none;", to: @unsubscribe_link)) %></span></p>
 | 
			
		||||
											</div>
 | 
			
		||||
											<!--[if mso]></td></tr></table><![endif]-->
 | 
			
		||||
											<!--[if (!mso)&(!IE)]><!-->
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
 | 
			
		||||
<feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom"
 | 
			
		||||
<feed xml:lang="<%= Gettext.language_tag() %>" xmlns="http://www.w3.org/2005/Atom"
 | 
			
		||||
      xmlns:thr="http://purl.org/syndication/thread/1.0"
 | 
			
		||||
      xmlns:georss="http://www.georss.org/georss"
 | 
			
		||||
      xmlns:activity="http://activitystrea.ms/spec/1.0/"
 | 
			
		||||
| 
						 | 
				
			
			@ -12,7 +12,7 @@
 | 
			
		|||
    <id><%= '#{Routes.tag_feed_url(@conn, :feed, @tag)}.rss' %></id>
 | 
			
		||||
    <title>#<%= @tag %></title>
 | 
			
		||||
 | 
			
		||||
    <subtitle>These are public toots tagged with #<%= @tag %>. You can interact with them if you have an account anywhere in the fediverse.</subtitle>
 | 
			
		||||
    <subtitle><%= Gettext.dpgettext("static_pages", "tag feed description", "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse.", tag: @tag) %></subtitle>
 | 
			
		||||
    <logo><%= feed_logo() %></logo>
 | 
			
		||||
    <updated><%= most_recent_update(@activities) %></updated>
 | 
			
		||||
    <link rel="self" href="<%= '#{Routes.tag_feed_url(@conn, :feed, @tag)}.atom'  %>" type="application/atom+xml"/>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,7 @@
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
    <title>#<%= @tag %></title>
 | 
			
		||||
    <description>These are public toots tagged with #<%= @tag %>. You can interact with them if you have an account anywhere in the fediverse.</description>
 | 
			
		||||
    <description><%= Gettext.dpgettext("static_pages", "tag feed description", "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse.", tag: @tag) %></description>
 | 
			
		||||
    <link><%= '#{Routes.tag_feed_url(@conn, :feed, @tag)}.rss' %></link>
 | 
			
		||||
    <webfeeds:logo><%= feed_logo() %></webfeeds:logo>
 | 
			
		||||
    <webfeeds:accentColor>2b90d9</webfeeds:accentColor>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
<html lang="<%= Pleroma.Web.Gettext.language_tag() %>">
 | 
			
		||||
  <head>
 | 
			
		||||
    <meta charset="utf-8">
 | 
			
		||||
    <meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
<html lang="<%= Pleroma.Web.Gettext.language_tag() %>">
 | 
			
		||||
  <head>
 | 
			
		||||
    <meta charset="utf-8">
 | 
			
		||||
    <title><%= @email.subject %></title>
 | 
			
		||||
| 
						 | 
				
			
			@ -7,4 +7,4 @@
 | 
			
		|||
  <body>
 | 
			
		||||
    <%= render @view_module, @view_template, assigns %>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
</html>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1 +1 @@
 | 
			
		|||
<h1>UNSUBSCRIBE FAILURE</h1>
 | 
			
		||||
<h1><%= Gettext.dpgettext("static_pages", "mailer unsubscribe failed message", "UNSUBSCRIBE FAILURE") %></h1>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1 +1 @@
 | 
			
		|||
<h1>UNSUBSCRIBE SUCCESSFUL</h1>
 | 
			
		||||
<h1><%= Gettext.dpgettext("static_pages", "mailer unsubscribe successful message", "UNSUBSCRIBE SUCCESSFUL") %></h1>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,11 +5,11 @@
 | 
			
		|||
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
 | 
			
		||||
<% end %>
 | 
			
		||||
 | 
			
		||||
<h2>Two-factor recovery</h2>
 | 
			
		||||
<h2><%= Gettext.dpgettext("static_pages", "mfa recover page title", "Two-factor recovery") %></h2>
 | 
			
		||||
 | 
			
		||||
<%= form_for @conn, Routes.mfa_verify_path(@conn, :verify), [as: "mfa"], fn f -> %>
 | 
			
		||||
<div class="input">
 | 
			
		||||
  <%= label f, :code, "Recovery code" %>
 | 
			
		||||
  <%= label f, :code, Gettext.dpgettext("static_pages", "mfa recover recovery code prompt", "Recovery code") %>
 | 
			
		||||
  <%= text_input f, :code, [autocomplete: false, autocorrect: "off", autocapitalize: "off", autofocus: true, spellcheck: false] %>
 | 
			
		||||
  <%= hidden_input f, :mfa_token, value: @mfa_token %>
 | 
			
		||||
  <%= hidden_input f, :state, value: @state %>
 | 
			
		||||
| 
						 | 
				
			
			@ -17,8 +17,8 @@
 | 
			
		|||
  <%= hidden_input f, :challenge_type, value: "recovery" %>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<%= submit "Verify" %>
 | 
			
		||||
<%= submit Gettext.dpgettext("static_pages", "mfa recover verify recovery code button", "Verify") %>
 | 
			
		||||
<% end %>
 | 
			
		||||
<a href="<%= Routes.mfa_path(@conn, :show, %{challenge_type: "totp", mfa_token: @mfa_token, state: @state, redirect_uri: @redirect_uri}) %>">
 | 
			
		||||
  Enter a two-factor code
 | 
			
		||||
  <%= Gettext.dpgettext("static_pages", "mfa recover use 2fa code link", "Enter a two-factor code") %>
 | 
			
		||||
</a>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,20 +5,20 @@
 | 
			
		|||
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
 | 
			
		||||
<% end %>
 | 
			
		||||
 | 
			
		||||
<h2>Two-factor authentication</h2>
 | 
			
		||||
<h2><%= Gettext.dpgettext("static_pages", "mfa auth page title", "Two-factor authentication") %></h2>
 | 
			
		||||
 | 
			
		||||
<%= form_for @conn, Routes.mfa_verify_path(@conn, :verify), [as: "mfa"], fn f -> %>
 | 
			
		||||
<div class="input">
 | 
			
		||||
  <%= label f, :code, "Authentication code" %>
 | 
			
		||||
  <%= text_input f, :code, [autocomplete: false, autocorrect: "off", autocapitalize: "off", autofocus: true, pattern: "[0-9]*", spellcheck: false] %>
 | 
			
		||||
  <%= label f, :code, Gettext.dpgettext("static_pages", "mfa auth code prompt", "Authentication code") %>
 | 
			
		||||
  <%= text_input f, :code, [autocomplete: "one-time-code", autocorrect: "off", autocapitalize: "off", autofocus: true, pattern: "[0-9]*", spellcheck: false] %>
 | 
			
		||||
  <%= hidden_input f, :mfa_token, value: @mfa_token %>
 | 
			
		||||
  <%= hidden_input f, :state, value: @state %>
 | 
			
		||||
  <%= hidden_input f, :redirect_uri, value: @redirect_uri %>
 | 
			
		||||
  <%= hidden_input f, :challenge_type, value: "totp" %>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<%= submit "Verify" %>
 | 
			
		||||
<%= submit Gettext.dpgettext("static_pages", "mfa auth verify code button", "Verify") %>
 | 
			
		||||
<% end %>
 | 
			
		||||
<a href="<%= Routes.mfa_path(@conn, :show, %{challenge_type: "recovery", mfa_token: @mfa_token, state: @state, redirect_uri: @redirect_uri}) %>">
 | 
			
		||||
  Enter a two-factor recovery code
 | 
			
		||||
  <%= Gettext.dpgettext("static_pages", "mfa auth page use recovery code link", "Enter a two-factor recovery code") %>
 | 
			
		||||
</a>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
<div class="scopes-input">
 | 
			
		||||
  <%= label @form, :scope, "The following permissions will be granted" %>
 | 
			
		||||
  <%= label @form, :scope, Gettext.dpgettext("static_pages", "oauth scopes message", "The following permissions will be granted") %>
 | 
			
		||||
  <div class="scopes">
 | 
			
		||||
    <%= for scope <- @available_scopes do %>
 | 
			
		||||
      <%# Note: using hidden input with `unchecked_value` in order to distinguish user's empty selection from `scope` param being omitted %>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
<h2>Sign in with external provider</h2>
 | 
			
		||||
<h2><%= Gettext.dpgettext("static_pages", "oauth external provider page title", "Sign in with external provider") %></h2>
 | 
			
		||||
 | 
			
		||||
<%= form_for @conn, Routes.o_auth_path(@conn, :prepare_request), [as: "authorization", method: "get"], fn f -> %>
 | 
			
		||||
  <div style="display: none">
 | 
			
		||||
| 
						 | 
				
			
			@ -10,6 +10,6 @@
 | 
			
		|||
  <%= hidden_input f, :state, value: @state %>
 | 
			
		||||
 | 
			
		||||
    <%= for strategy <- Pleroma.Config.oauth_consumer_strategies() do %>
 | 
			
		||||
      <%= submit "Sign in with #{String.capitalize(strategy)}", name: "provider", value: strategy %>
 | 
			
		||||
      <%= submit Gettext.dpgettext("static_pages", "oauth external provider sign in button", "Sign in with %{strategy}", strategy: String.capitalize(strategy)), name: "provider", value: strategy %>
 | 
			
		||||
    <% end %>
 | 
			
		||||
<% end %>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,2 +1,2 @@
 | 
			
		|||
<h1>Successfully authorized</h1>
 | 
			
		||||
<h2>Token code is <br><%= @auth.token %></h2>
 | 
			
		||||
<h1><%= Gettext.dpgettext("static_pages", "oauth authorized page title", "Successfully authorized") %></h1>
 | 
			
		||||
<h2><%= raw Gettext.dpgettext("static_pages", "oauth token code message", "Token code is <br>%{token}", token: safe_to_string(html_escape(@auth.token))) %></h2>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,2 +1,2 @@
 | 
			
		|||
<h1>Authorization exists</h1>
 | 
			
		||||
<h2>Access token is <br><%= @token.token %></h2>
 | 
			
		||||
<h1><%= Gettext.dpgettext("static_pages", "oauth authorization exists page title", "Authorization exists") %></h1>
 | 
			
		||||
<h2><%= raw Gettext.dpgettext("static_pages", "oauth token code message", "Token code is <br>%{token}", token: safe_to_string(html_escape(@token.token))) %></h2>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,34 +5,34 @@
 | 
			
		|||
  <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
 | 
			
		||||
<% end %>
 | 
			
		||||
 | 
			
		||||
<h2>Registration Details</h2>
 | 
			
		||||
<h2><%= Gettext.dpgettext("static_pages", "oauth register page title", "Registration Details") %></h2>
 | 
			
		||||
 | 
			
		||||
<p>If you'd like to register a new account, please provide the details below.</p>
 | 
			
		||||
<p><%= Gettext.dpgettext("static_pages", "oauth register page fill form prompt", "If you'd like to register a new account, please provide the details below.") %></p>
 | 
			
		||||
<%= form_for @conn, Routes.o_auth_path(@conn, :register), [as: "authorization"], fn f -> %>
 | 
			
		||||
 | 
			
		||||
<div class="input">
 | 
			
		||||
  <%= label f, :nickname, "Nickname" %>
 | 
			
		||||
  <%= text_input f, :nickname, value: @nickname %>
 | 
			
		||||
  <%= label f, :nickname, Gettext.dpgettext("static_pages", "oauth register page nickname prompt", "Nickname") %>
 | 
			
		||||
  <%= text_input f, :nickname, value: @nickname, autocomplete: "username" %>
 | 
			
		||||
</div>
 | 
			
		||||
<div class="input">
 | 
			
		||||
  <%= label f, :email, "Email" %>
 | 
			
		||||
  <%= text_input f, :email, value: @email %>
 | 
			
		||||
  <%= label f, :email, Gettext.dpgettext("static_pages", "oauth register page email prompt", "Email") %>
 | 
			
		||||
  <%= text_input f, :email, value: @email, autocomplete: "email" %>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<%= submit "Proceed as new user", name: "op", value: "register" %>
 | 
			
		||||
<%= submit Gettext.dpgettext("static_pages", "oauth register page register button", "Proceed as new user"), name: "op", value: "register" %>
 | 
			
		||||
 | 
			
		||||
<p>Alternatively, sign in to connect to existing account.</p>
 | 
			
		||||
<p><%= Gettext.dpgettext("static_pages", "oauth register page login prompt", "Alternatively, sign in to connect to existing account.") %></p>
 | 
			
		||||
 | 
			
		||||
<div class="input">
 | 
			
		||||
  <%= label f, :name, "Name or email" %>
 | 
			
		||||
  <%= text_input f, :name %>
 | 
			
		||||
  <%= label f, :name, Gettext.dpgettext("static_pages", "oauth register page login username prompt", "Name or email") %>
 | 
			
		||||
  <%= text_input f, :name, autocomplete: "username" %>
 | 
			
		||||
</div>
 | 
			
		||||
<div class="input">
 | 
			
		||||
  <%= label f, :password, "Password" %>
 | 
			
		||||
  <%= password_input f, :password %>
 | 
			
		||||
  <%= label f, :password, Gettext.dpgettext("static_pages", "oauth register page login password prompt", "Password") %>
 | 
			
		||||
  <%= password_input f, :password, autocomplete: "password" %>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<%= submit "Proceed as existing user", name: "op", value: "connect" %>
 | 
			
		||||
<%= submit Gettext.dpgettext("static_pages", "oauth register page login button", "Proceed as existing user"), name: "op", value: "connect" %>
 | 
			
		||||
 | 
			
		||||
<%= hidden_input f, :client_id, value: @client_id %>
 | 
			
		||||
<%= hidden_input f, :redirect_uri, value: @redirect_uri %>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,36 +20,38 @@
 | 
			
		|||
 | 
			
		||||
<div class="container__content">
 | 
			
		||||
  <%= if @app do %>
 | 
			
		||||
    <p>Application <strong><%= @app.client_name %></strong> is requesting access to your account.</p>
 | 
			
		||||
    <p><%= raw Gettext.dpgettext("static_pages", "oauth authorize message", "Application <strong>%{client_name}</strong> is requesting access to your account.", client_name: safe_to_string(html_escape(@app.client_name))) %></p>
 | 
			
		||||
    <%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f}) %>
 | 
			
		||||
  <% end %>
 | 
			
		||||
 | 
			
		||||
  <%= if @user do %>
 | 
			
		||||
    <div class="actions">
 | 
			
		||||
      <a class="button button--cancel" href="/">Cancel</a>
 | 
			
		||||
      <%= submit "Approve", class: "button--approve" %>
 | 
			
		||||
      <a class="button button--cancel" href="/">
 | 
			
		||||
        <%= Gettext.dpgettext("static_pages", "oauth authorize cancel button", "Cancel") %>
 | 
			
		||||
      </a>
 | 
			
		||||
      <%= submit Gettext.dpgettext("static_pages", "oauth authorize approve button", "Approve"), class: "button--approve" %>
 | 
			
		||||
    </div>
 | 
			
		||||
  <% else %>
 | 
			
		||||
    <%= if @params["registration"] in ["true", true] do %>
 | 
			
		||||
      <h3>This is the first time you visit! Please enter your Pleroma handle.</h3>
 | 
			
		||||
      <p>Choose carefully! You won't be able to change this later. You will be able to change your display name, though.</p>
 | 
			
		||||
      <h3><%= Gettext.dpgettext("static_pages", "oauth register page title", "This is the first time you visit! Please enter your Pleroma handle.") %></h3>
 | 
			
		||||
      <p><%= Gettext.dpgettext("static_pages", "oauth register nickname unchangeable warning", "Choose carefully! You won't be able to change this later. You will be able to change your display name, though.") %></p>
 | 
			
		||||
      <div class="input">
 | 
			
		||||
        <%= label f, :nickname, "Pleroma Handle" %>
 | 
			
		||||
        <%= text_input f, :nickname, placeholder: "lain" %>
 | 
			
		||||
        <%= label f, :nickname, Gettext.dpgettext("static_pages", "oauth register nickname prompt", "Pleroma Handle") %>
 | 
			
		||||
        <%= text_input f, :nickname, placeholder: "lain", autocomplete: "username" %>
 | 
			
		||||
      </div>
 | 
			
		||||
      <%= hidden_input f, :name, value: @params["name"] %>
 | 
			
		||||
      <%= hidden_input f, :password, value: @params["password"] %>
 | 
			
		||||
      <br>
 | 
			
		||||
    <% else %>
 | 
			
		||||
      <div class="input">
 | 
			
		||||
        <%= label f, :name, "Username" %>
 | 
			
		||||
        <%= label f, :name, Gettext.dpgettext("static_pages", "oauth login username prompt", "Username") %>
 | 
			
		||||
        <%= text_input f, :name %>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="input">
 | 
			
		||||
        <%= label f, :password, "Password" %>
 | 
			
		||||
        <%= label f, :password, Gettext.dpgettext("static_pages", "oauth login password prompt", "Password") %>
 | 
			
		||||
        <%= password_input f, :password %>
 | 
			
		||||
      </div>
 | 
			
		||||
      <%= submit "Log In" %>
 | 
			
		||||
      <%= submit Gettext.dpgettext("static_pages", "oauth login button", "Log In") %>
 | 
			
		||||
    <% end %>
 | 
			
		||||
  <% end %>
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,7 +5,7 @@
 | 
			
		|||
    <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>
 | 
			
		||||
      <button type="submit" class="collapse"><%= Gettext.dpgettext("static_pages", "static fe profile page remote follow button", "Remote follow") %></button>
 | 
			
		||||
    </form>
 | 
			
		||||
    <%= raw Formatter.emojify(@user.name, @user.emoji) %> |
 | 
			
		||||
    <%= link "@#{@user.nickname}@#{Endpoint.host()}", to: (@user.uri || @user.ap_id) %>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1 +1 @@
 | 
			
		|||
<h2>Invalid Token</h2>
 | 
			
		||||
<h2><%= Gettext.dpgettext("static_pages", "password reset invalid token message", "Invalid Token") %></h2>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,13 +1,13 @@
 | 
			
		|||
<h2>Password Reset for <%= @user.nickname %></h2>
 | 
			
		||||
<%= form_for @conn, Routes.reset_password_path(@conn, :do_reset), [as: "data"], fn f -> %>
 | 
			
		||||
  <div class="form-row">
 | 
			
		||||
    <%= label f, :password, "Password" %>
 | 
			
		||||
    <%= label f, :password, Gettext.dpgettext("static_pages", "password reset form password prompt", "Password") %>
 | 
			
		||||
    <%= password_input f, :password %>
 | 
			
		||||
  </div>
 | 
			
		||||
  <div class="form-row">
 | 
			
		||||
    <%= label f, :password_confirmation, "Confirmation" %>
 | 
			
		||||
    <%= label f, :password_confirmation, Gettext.dpgettext("static_pages", "password reset form confirm password prompt", "Confirmation") %>
 | 
			
		||||
    <%= password_input f, :password_confirmation %>
 | 
			
		||||
  </div>
 | 
			
		||||
  <%= hidden_input f, :token, value: @token.token %>
 | 
			
		||||
  <%= submit "Reset" %>
 | 
			
		||||
  <%= submit Gettext.dpgettext("static_pages", "password reset button", "Reset") %>
 | 
			
		||||
<% end %>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,2 +1,6 @@
 | 
			
		|||
<h2>Password reset failed</h2>
 | 
			
		||||
<h3><a href="<%= Pleroma.Web.Endpoint.url() %>">Homepage</a></h3>
 | 
			
		||||
<h2><%= Gettext.dpgettext("static_pages", "password reset failed message", "Password reset failed") %></h2>
 | 
			
		||||
<h3>
 | 
			
		||||
  <a href="<%= Pleroma.Web.Endpoint.url() %>">
 | 
			
		||||
    <%= Gettext.dpgettext("static_pages", "password reset failed homepage link", "Homepage") %>
 | 
			
		||||
  </a>
 | 
			
		||||
</h3>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,2 +1,2 @@
 | 
			
		|||
<h2>Password changed!</h2>
 | 
			
		||||
<h3><a href="<%= Pleroma.Web.Endpoint.url() %>">Homepage</a></h3>
 | 
			
		||||
<h2><%= Gettext.dpgettext("static_pages", "password reset successful message", "Password changed!") %></h2>
 | 
			
		||||
<h3><a href="<%= Pleroma.Web.Endpoint.url() %>"><%= Gettext.dpgettext("static_pages", "password reset successful homepage link", "Homepage") %></a></h3>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,11 +1,11 @@
 | 
			
		|||
<%= if @error == :error do %>
 | 
			
		||||
    <h2>Error fetching user</h2>
 | 
			
		||||
    <h2><%= Gettext.dpgettext("static_pages", "remote follow error", "Error fetching user") %></h2>
 | 
			
		||||
<% else %>
 | 
			
		||||
    <h2>Remote follow</h2>
 | 
			
		||||
    <h2><%= Gettext.dpgettext("static_pages", "remote follow header", "Remote follow") %></h2>
 | 
			
		||||
    <img height="128" width="128" src="<%= avatar_url(@followee) %>">
 | 
			
		||||
    <p><%= @followee.nickname %></p>
 | 
			
		||||
    <%= form_for @conn, Routes.remote_follow_path(@conn, :do_follow), [as: "user"], fn f -> %>
 | 
			
		||||
    <%= hidden_input f, :id, value: @followee.id %>
 | 
			
		||||
    <%= submit "Authorize" %>
 | 
			
		||||
    <%= submit Gettext.dpgettext("static_pages", "remote follow authorization button", "Authorize") %>
 | 
			
		||||
    <% end %>
 | 
			
		||||
<% end %>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,14 +1,14 @@
 | 
			
		|||
<%= if @error do %>
 | 
			
		||||
<h2><%= @error %></h2>
 | 
			
		||||
<% end %>
 | 
			
		||||
<h2>Log in to follow</h2>
 | 
			
		||||
<h2><%= Gettext.dpgettext("static_pages", "remote follow header, need login", "Log in to follow") %></h2>
 | 
			
		||||
<p><%= @followee.nickname %></p>
 | 
			
		||||
<img height="128" width="128" src="<%= avatar_url(@followee) %>">
 | 
			
		||||
<%= form_for @conn, Routes.remote_follow_path(@conn, :do_follow), [as: "authorization"], fn f -> %>
 | 
			
		||||
<%= text_input f, :name, placeholder: "Username", required: true %>
 | 
			
		||||
<%= text_input f, :name, placeholder: Gettext.dpgettext("static_pages", "placeholder text for username entry", "Username"), required: true, autocomplete: "username" %>
 | 
			
		||||
<br>
 | 
			
		||||
<%= password_input f, :password, placeholder: "Password", required: true %>
 | 
			
		||||
<%= password_input f, :password, placeholder: Gettext.dpgettext("static_pages", "placeholder text for password entry", "Password"), required: true, autocomplete: "password" %>
 | 
			
		||||
<br>
 | 
			
		||||
<%= hidden_input f, :id, value: @followee.id %>
 | 
			
		||||
<%= submit "Authorize" %>
 | 
			
		||||
<%= submit Gettext.dpgettext("static_pages", "remote follow authorization button for login", "Authorize") %>
 | 
			
		||||
<% end %>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,13 +1,13 @@
 | 
			
		|||
<%= if @error do %>
 | 
			
		||||
<h2><%= @error %></h2>
 | 
			
		||||
<% end %>
 | 
			
		||||
<h2>Two-factor authentication</h2>
 | 
			
		||||
<h2><%= Gettext.dpgettext("static_pages", "remote follow mfa header", "Two-factor authentication") %></h2>
 | 
			
		||||
<p><%= @followee.nickname %></p>
 | 
			
		||||
<img height="128" width="128" src="<%= avatar_url(@followee) %>">
 | 
			
		||||
<%= form_for @conn, Routes.remote_follow_path(@conn, :do_follow), [as: "mfa"], fn f -> %>
 | 
			
		||||
<%= text_input f, :code, placeholder: "Authentication code", required: true %>
 | 
			
		||||
<%= text_input f, :code, placeholder: Gettext.dpgettext("static_pages", "placeholder text for auth code entry", "Authentication code"), required: true %>
 | 
			
		||||
<br>
 | 
			
		||||
<%= hidden_input f, :id, value: @followee.id %>
 | 
			
		||||
<%= hidden_input f, :token, value: @mfa_token %>
 | 
			
		||||
<%= submit "Authorize" %>
 | 
			
		||||
<%= submit Gettext.dpgettext("static_pages", "remote follow authorization button for mfa", "Authorize") %>
 | 
			
		||||
<% end %>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,5 @@
 | 
			
		|||
<%= if @error do %>
 | 
			
		||||
<p>Error following account</p>
 | 
			
		||||
<p><%= Gettext.dpgettext("static_pages", "remote follow error", "Error following account") %></p>
 | 
			
		||||
<% else %>
 | 
			
		||||
<h2>Account followed!</h2>
 | 
			
		||||
<h2><%= Gettext.dpgettext("static_pages", "remote follow success", "Account followed!") %></h2>
 | 
			
		||||
<% end %>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,10 +1,10 @@
 | 
			
		|||
<%= if @error do %>
 | 
			
		||||
  <h2>Error: <%= @error %></h2>
 | 
			
		||||
  <h2><%= Gettext.dpgettext("static_pages", "remote follow error", "Error: %{error}", error: @error) %></h2>
 | 
			
		||||
<% else %>
 | 
			
		||||
  <h2>Remotely follow <%= @nickname %></h2>
 | 
			
		||||
  <h2><%= Gettext.dpgettext("static_pages", "remote follow header", "Remotely follow %{nickname}", nickname: @nickname) %></h2>
 | 
			
		||||
  <%= form_for @conn, Routes.util_path(@conn, :remote_subscribe), [as: "user"], fn f -> %>
 | 
			
		||||
  <%= hidden_input f, :nickname, value: @nickname %>
 | 
			
		||||
  <%= text_input f, :profile, placeholder: "Your account ID, e.g. lain@quitter.se" %>
 | 
			
		||||
  <%= submit "Follow" %>
 | 
			
		||||
  <%= text_input f, :profile, placeholder: Gettext.dpgettext("static_pages", "placeholder text for account id", "Your account ID, e.g. lain@quitter.se") %>
 | 
			
		||||
  <%= submit Gettext.dpgettext("static_pages", "remote follow authorization button for following with a remote account", "Follow") %>
 | 
			
		||||
  <% end %>
 | 
			
		||||
<% end %>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,6 +12,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
 | 
			
		|||
  alias Pleroma.UserInviteToken
 | 
			
		||||
 | 
			
		||||
  def register_user(params, opts \\ []) do
 | 
			
		||||
    fallback_language = Gettext.get_locale()
 | 
			
		||||
 | 
			
		||||
    params =
 | 
			
		||||
      params
 | 
			
		||||
      |> Map.take([:email, :token, :password])
 | 
			
		||||
| 
						 | 
				
			
			@ -20,6 +22,10 @@ def register_user(params, opts \\ []) do
 | 
			
		|||
      |> Map.put(:name, Map.get(params, :fullname, params[:username]))
 | 
			
		||||
      |> Map.put(:password_confirmation, params[:password])
 | 
			
		||||
      |> Map.put(:registration_reason, params[:reason])
 | 
			
		||||
      |> Map.put(
 | 
			
		||||
        :language,
 | 
			
		||||
        Pleroma.Web.Gettext.normalize_locale(params[:language]) || fallback_language
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
    if Pleroma.Config.get([:instance, :registrations_open]) do
 | 
			
		||||
      create_user(params, opts)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,4 +5,5 @@
 | 
			
		|||
defmodule Pleroma.Web.TwitterAPI.PasswordView do
 | 
			
		||||
  use Pleroma.Web, :view
 | 
			
		||||
  import Phoenix.HTML.Form
 | 
			
		||||
  alias Pleroma.Web.Gettext
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,6 +5,7 @@
 | 
			
		|||
defmodule Pleroma.Web.TwitterAPI.RemoteFollowView do
 | 
			
		||||
  use Pleroma.Web, :view
 | 
			
		||||
  import Phoenix.HTML.Form
 | 
			
		||||
  alias Pleroma.Web.Gettext
 | 
			
		||||
 | 
			
		||||
  defdelegate avatar_url(user), to: Pleroma.User
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,6 +7,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilView do
 | 
			
		|||
  import Phoenix.HTML.Form
 | 
			
		||||
  alias Pleroma.Config
 | 
			
		||||
  alias Pleroma.Web.Endpoint
 | 
			
		||||
  alias Pleroma.Web.Gettext
 | 
			
		||||
 | 
			
		||||
  def status_net_config(instance) do
 | 
			
		||||
    """
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,7 @@ defmodule Pleroma.Web.EmailView do
 | 
			
		|||
  use Pleroma.Web, :view
 | 
			
		||||
  import Phoenix.HTML
 | 
			
		||||
  import Phoenix.HTML.Link
 | 
			
		||||
  alias Pleroma.Web.Gettext
 | 
			
		||||
 | 
			
		||||
  def avatar_url(user) do
 | 
			
		||||
    Pleroma.User.avatar_url(user)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,4 +4,5 @@
 | 
			
		|||
 | 
			
		||||
defmodule Pleroma.Web.Mailer.SubscriptionView do
 | 
			
		||||
  use Pleroma.Web, :view
 | 
			
		||||
  alias Pleroma.Web.Gettext
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										25
									
								
								lib/pleroma/workers/search_indexing_worker.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								lib/pleroma/workers/search_indexing_worker.ex
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,25 @@
 | 
			
		|||
defmodule Pleroma.Workers.SearchIndexingWorker do
 | 
			
		||||
  use Pleroma.Workers.WorkerHelper, queue: "search_indexing"
 | 
			
		||||
 | 
			
		||||
  @impl Oban.Worker
 | 
			
		||||
 | 
			
		||||
  def perform(%Job{args: %{"op" => "add_to_index", "activity" => activity_id}}) do
 | 
			
		||||
    activity = Pleroma.Activity.get_by_id_with_object(activity_id)
 | 
			
		||||
 | 
			
		||||
    search_module = Pleroma.Config.get([Pleroma.Search, :module])
 | 
			
		||||
 | 
			
		||||
    search_module.add_to_index(activity)
 | 
			
		||||
 | 
			
		||||
    :ok
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def perform(%Job{args: %{"op" => "remove_from_index", "object" => object_id}}) do
 | 
			
		||||
    object = Pleroma.Object.get_by_id(object_id)
 | 
			
		||||
 | 
			
		||||
    search_module = Pleroma.Config.get([Pleroma.Search, :module])
 | 
			
		||||
 | 
			
		||||
    search_module.remove_from_index(object)
 | 
			
		||||
 | 
			
		||||
    :ok
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										10
									
								
								mix.exs
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								mix.exs
									
									
									
									
									
								
							| 
						 | 
				
			
			@ -123,7 +123,10 @@ defp deps do
 | 
			
		|||
      {:ecto_sql, "~> 3.6.2"},
 | 
			
		||||
      {:postgrex, ">= 0.15.5"},
 | 
			
		||||
      {:oban, "~> 2.3.4"},
 | 
			
		||||
      {:gettext, "~> 0.18"},
 | 
			
		||||
      {:gettext,
 | 
			
		||||
       git: "https://github.com/tusooa/gettext.git",
 | 
			
		||||
       ref: "72fb2496b6c5280ed911bdc3756890e7f38a4808",
 | 
			
		||||
       override: true},
 | 
			
		||||
      {:bcrypt_elixir, "~> 2.2"},
 | 
			
		||||
      {:trailing_format_plug, "~> 0.0.7"},
 | 
			
		||||
      {:fast_sanitize, "~> 0.2.0"},
 | 
			
		||||
| 
						 | 
				
			
			@ -200,6 +203,7 @@ defp deps do
 | 
			
		|||
      {:nimble_parsec, "~> 1.0", override: true},
 | 
			
		||||
      {:phoenix_live_dashboard, "~> 0.6.2"},
 | 
			
		||||
      {:ecto_psql_extras, "~> 0.6"},
 | 
			
		||||
      {:elasticsearch, "~> 1.0.0"},
 | 
			
		||||
 | 
			
		||||
      # indirect dependency version override
 | 
			
		||||
      {:plug, "~> 1.10.4", override: true},
 | 
			
		||||
| 
						 | 
				
			
			@ -248,9 +252,10 @@ defp version(version) do
 | 
			
		|||
    identifier_filter = ~r/[^0-9a-z\-]+/i
 | 
			
		||||
 | 
			
		||||
    git_available? = match?({_output, 0}, System.cmd("sh", ["-c", "command -v git"]))
 | 
			
		||||
    dotgit_present? = File.exists?(".git")
 | 
			
		||||
 | 
			
		||||
    git_pre_release =
 | 
			
		||||
      if git_available? do
 | 
			
		||||
      if git_available? and dotgit_present? do
 | 
			
		||||
        {tag, tag_err} =
 | 
			
		||||
          System.cmd("git", ["describe", "--tags", "--abbrev=0"], stderr_to_stdout: true)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -277,6 +282,7 @@ defp version(version) do
 | 
			
		|||
    # Branch name as pre-release version component, denoted with a dot
 | 
			
		||||
    branch_name =
 | 
			
		||||
      with true <- git_available?,
 | 
			
		||||
           true <- dotgit_present?,
 | 
			
		||||
           {branch_name, 0} <- System.cmd("git", ["rev-parse", "--abbrev-ref", "HEAD"]),
 | 
			
		||||
           branch_name <- String.trim(branch_name),
 | 
			
		||||
           branch_name <- System.get_env("PLEROMA_BUILD_BRANCH") || branch_name,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										2
									
								
								mix.lock
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								mix.lock
									
									
									
									
									
								
							| 
						 | 
				
			
			@ -57,7 +57,7 @@
 | 
			
		|||
  "gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm", "29bd14a88030980849c7ed2447b8db6d6c9278a28b11a44cafe41b791205440f"},
 | 
			
		||||
  "gen_stage": {:hex, :gen_stage, "0.14.3", "d0c66f1c87faa301c1a85a809a3ee9097a4264b2edf7644bf5c123237ef732bf", [:mix], [], "hexpm"},
 | 
			
		||||
  "gen_state_machine": {:hex, :gen_state_machine, "2.0.5", "9ac15ec6e66acac994cc442dcc2c6f9796cf380ec4b08267223014be1c728a95", [:mix], [], "hexpm"},
 | 
			
		||||
  "gettext": {:hex, :gettext, "0.18.2", "7df3ea191bb56c0309c00a783334b288d08a879f53a7014341284635850a6e55", [:mix], [], "hexpm", "f9f537b13d4fdd30f3039d33cb80144c3aa1f8d9698e47d7bcbcc8df93b1f5c5"},
 | 
			
		||||
  "gettext": {:git, "https://github.com/tusooa/gettext.git", "72fb2496b6c5280ed911bdc3756890e7f38a4808", [ref: "72fb2496b6c5280ed911bdc3756890e7f38a4808"]},
 | 
			
		||||
  "gun": {:hex, :gun, "2.0.0-rc.2", "7c489a32dedccb77b6e82d1f3c5a7dadfbfa004ec14e322cdb5e579c438632d2", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "6b9d1eae146410d727140dbf8b404b9631302ecc2066d1d12f22097ad7d254fc"},
 | 
			
		||||
  "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"},
 | 
			
		||||
  "hpax": {:hex, :hpax, "0.1.1", "2396c313683ada39e98c20a75a82911592b47e5c24391363343bde74f82396ca", [:mix], [], "hexpm", "0ae7d5a0b04a8a60caf7a39fcf3ec476f35cc2cc16c05abea730d3ce6ac6c826"},
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,20 +1,22 @@
 | 
			
		|||
{
 | 
			
		||||
  "properties": {
 | 
			
		||||
    "_timestamp": {
 | 
			
		||||
      "type": "date",
 | 
			
		||||
      "index": true
 | 
			
		||||
    },
 | 
			
		||||
    "instance": {
 | 
			
		||||
      "type": "keyword"
 | 
			
		||||
    },
 | 
			
		||||
    "content": {
 | 
			
		||||
      "type": "text"
 | 
			
		||||
    },
 | 
			
		||||
    "hashtags": {
 | 
			
		||||
      "type": "keyword"
 | 
			
		||||
    },
 | 
			
		||||
    "user": {
 | 
			
		||||
      "type": "text"
 | 
			
		||||
  "mappings": {
 | 
			
		||||
    "properties": {
 | 
			
		||||
      "_timestamp": {
 | 
			
		||||
        "type": "date",
 | 
			
		||||
        "index": true
 | 
			
		||||
      },
 | 
			
		||||
      "instance": {
 | 
			
		||||
        "type": "keyword"
 | 
			
		||||
      },
 | 
			
		||||
      "content": {
 | 
			
		||||
        "type": "text"
 | 
			
		||||
      },
 | 
			
		||||
      "hashtags": {
 | 
			
		||||
        "type": "keyword"
 | 
			
		||||
      },
 | 
			
		||||
      "user": {
 | 
			
		||||
        "type": "text"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										185
									
								
								priv/gettext/default.pot
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										185
									
								
								priv/gettext/default.pot
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,185 @@
 | 
			
		|||
## This file is a PO Template file.
 | 
			
		||||
##
 | 
			
		||||
## "msgid"s here are often extracted from source code.
 | 
			
		||||
## Add new translations manually only if they're dynamic
 | 
			
		||||
## translations that can't be statically extracted.
 | 
			
		||||
##
 | 
			
		||||
## Run "mix gettext.extract" to bring this file up to
 | 
			
		||||
## date. Leave "msgstr"s empty as changing them here as no
 | 
			
		||||
## effect: edit them in PO (.po) files instead.
 | 
			
		||||
msgid ""
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:122
 | 
			
		||||
msgid "%{name} - %{count} is not a multiple of %{multiple}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:131
 | 
			
		||||
msgid "%{name} - %{value} is larger than exclusive maximum %{max}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:140
 | 
			
		||||
msgid "%{name} - %{value} is larger than inclusive maximum %{max}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:149
 | 
			
		||||
msgid "%{name} - %{value} is smaller than exclusive minimum %{min}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:158
 | 
			
		||||
msgid "%{name} - %{value} is smaller than inclusive minimum %{min}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:102
 | 
			
		||||
msgid "%{name} - Array items must be unique."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:114
 | 
			
		||||
msgid "%{name} - Array length %{length} is larger than maxItems: %{}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:106
 | 
			
		||||
msgid "%{name} - Array length %{length} is smaller than minItems: %{min}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:166
 | 
			
		||||
msgid "%{name} - Invalid %{type}. Got: %{value}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:174
 | 
			
		||||
msgid "%{name} - Invalid format. Expected %{format}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:51
 | 
			
		||||
msgid "%{name} - Invalid schema.type. Got: %{type}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:178
 | 
			
		||||
msgid "%{name} - Invalid value for enum."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:95
 | 
			
		||||
msgid "%{name} - String length is larger than maxLength: %{length}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:88
 | 
			
		||||
msgid "%{name} - String length is smaller than minLength: %{length}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:63
 | 
			
		||||
msgid "%{name} - null value where %{type} expected."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:60
 | 
			
		||||
msgid "%{name} - null value."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:182
 | 
			
		||||
msgid "Failed to cast to any schema in %{polymorphic_type}"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:71
 | 
			
		||||
msgid "Failed to cast value as %{invalid_schema}. Value must be castable using `allOf` schemas listed."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:84
 | 
			
		||||
msgid "Failed to cast value to one of: %{failed_schemas}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:78
 | 
			
		||||
msgid "Failed to cast value using any of: %{failed_schemas}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:212
 | 
			
		||||
msgid "Invalid value for header: %{name}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:204
 | 
			
		||||
msgid "Missing field: %{name}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:208
 | 
			
		||||
msgid "Missing header: %{name}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:196
 | 
			
		||||
msgid "No value provided for required discriminator `%{field}`."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:216
 | 
			
		||||
msgid "Object property count %{property_count} is greater than maxProperties: %{max_properties}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:224
 | 
			
		||||
msgid "Object property count %{property_count} is less than minProperties: %{min_properties}"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/static_fe/static_fe/error.html.eex:2
 | 
			
		||||
msgid "Oops"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:188
 | 
			
		||||
msgid "Unexpected field: %{name}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:200
 | 
			
		||||
msgid "Unknown schema: %{name}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:192
 | 
			
		||||
msgid "Value used as discriminator for `%{field}` matches no schemas."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/embed/show.html.eex:43
 | 
			
		||||
#: lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex:37
 | 
			
		||||
msgid "announces"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/embed/show.html.eex:44
 | 
			
		||||
#: lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex:38
 | 
			
		||||
msgid "likes"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/embed/show.html.eex:42
 | 
			
		||||
#: lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex:36
 | 
			
		||||
msgid "replies"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/embed/show.html.eex:27
 | 
			
		||||
#: lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex:22
 | 
			
		||||
msgid "sensitive media"
 | 
			
		||||
msgstr ""
 | 
			
		||||
							
								
								
									
										186
									
								
								priv/gettext/en_test/LC_MESSAGES/default.po
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										186
									
								
								priv/gettext/en_test/LC_MESSAGES/default.po
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,186 @@
 | 
			
		|||
## "msgid"s in this file come from POT (.pot) files.
 | 
			
		||||
##
 | 
			
		||||
## Do not add, change, or remove "msgid"s manually here as
 | 
			
		||||
## they're tied to the ones in the corresponding POT file
 | 
			
		||||
## (with the same domain).
 | 
			
		||||
##
 | 
			
		||||
## Use "mix gettext.extract --merge" or "mix gettext.merge"
 | 
			
		||||
## to merge POT files into PO files.
 | 
			
		||||
msgid ""
 | 
			
		||||
msgstr ""
 | 
			
		||||
"Language: en_test\n"
 | 
			
		||||
"Plural-Forms: nplurals=2\n"
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:122
 | 
			
		||||
msgid "%{name} - %{count} is not a multiple of %{multiple}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:131
 | 
			
		||||
msgid "%{name} - %{value} is larger than exclusive maximum %{max}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:140
 | 
			
		||||
msgid "%{name} - %{value} is larger than inclusive maximum %{max}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:149
 | 
			
		||||
msgid "%{name} - %{value} is smaller than exclusive minimum %{min}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:158
 | 
			
		||||
msgid "%{name} - %{value} is smaller than inclusive minimum %{min}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:102
 | 
			
		||||
msgid "%{name} - Array items must be unique."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:114
 | 
			
		||||
msgid "%{name} - Array length %{length} is larger than maxItems: %{}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:106
 | 
			
		||||
msgid "%{name} - Array length %{length} is smaller than minItems: %{min}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:166
 | 
			
		||||
msgid "%{name} - Invalid %{type}. Got: %{value}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:174
 | 
			
		||||
msgid "%{name} - Invalid format. Expected %{format}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:51
 | 
			
		||||
msgid "%{name} - Invalid schema.type. Got: %{type}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:178
 | 
			
		||||
msgid "%{name} - Invalid value for enum."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:95
 | 
			
		||||
msgid "%{name} - String length is larger than maxLength: %{length}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:88
 | 
			
		||||
msgid "%{name} - String length is smaller than minLength: %{length}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:63
 | 
			
		||||
msgid "%{name} - null value where %{type} expected."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:60
 | 
			
		||||
msgid "%{name} - null value."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:182
 | 
			
		||||
msgid "Failed to cast to any schema in %{polymorphic_type}"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:71
 | 
			
		||||
msgid "Failed to cast value as %{invalid_schema}. Value must be castable using `allOf` schemas listed."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:84
 | 
			
		||||
msgid "Failed to cast value to one of: %{failed_schemas}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:78
 | 
			
		||||
msgid "Failed to cast value using any of: %{failed_schemas}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:212
 | 
			
		||||
msgid "Invalid value for header: %{name}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:204
 | 
			
		||||
msgid "Missing field: %{name}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:208
 | 
			
		||||
msgid "Missing header: %{name}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:196
 | 
			
		||||
msgid "No value provided for required discriminator `%{field}`."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:216
 | 
			
		||||
msgid "Object property count %{property_count} is greater than maxProperties: %{max_properties}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:224
 | 
			
		||||
msgid "Object property count %{property_count} is less than minProperties: %{min_properties}"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/static_fe/static_fe/error.html.eex:2
 | 
			
		||||
msgid "Oops"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:188
 | 
			
		||||
msgid "Unexpected field: %{name}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:200
 | 
			
		||||
msgid "Unknown schema: %{name}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/api_spec/render_error.ex:192
 | 
			
		||||
msgid "Value used as discriminator for `%{field}` matches no schemas."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/embed/show.html.eex:43
 | 
			
		||||
#: lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex:37
 | 
			
		||||
msgid "announces"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/embed/show.html.eex:44
 | 
			
		||||
#: lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex:38
 | 
			
		||||
msgid "likes"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/embed/show.html.eex:42
 | 
			
		||||
#: lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex:36
 | 
			
		||||
msgid "replies"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/embed/show.html.eex:27
 | 
			
		||||
#: lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex:22
 | 
			
		||||
msgid "sensitive media"
 | 
			
		||||
msgstr ""
 | 
			
		||||
							
								
								
									
										557
									
								
								priv/gettext/en_test/LC_MESSAGES/errors.po
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										557
									
								
								priv/gettext/en_test/LC_MESSAGES/errors.po
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,557 @@
 | 
			
		|||
## "msgid"s in this file come from POT (.pot) files.
 | 
			
		||||
##
 | 
			
		||||
## Do not add, change, or remove "msgid"s manually here as
 | 
			
		||||
## they're tied to the ones in the corresponding POT file
 | 
			
		||||
## (with the same domain).
 | 
			
		||||
##
 | 
			
		||||
## Use "mix gettext.extract --merge" or "mix gettext.merge"
 | 
			
		||||
## to merge POT files into PO files.
 | 
			
		||||
msgid ""
 | 
			
		||||
msgstr ""
 | 
			
		||||
"Language: en_test\n"
 | 
			
		||||
"Plural-Forms: nplurals=2\n"
 | 
			
		||||
 | 
			
		||||
msgid "can't be blank"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "has already been taken"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "is invalid"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "has invalid format"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "has an invalid entry"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "is reserved"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "does not match confirmation"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "is still associated with this entry"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "are still associated with this entry"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "should be %{count} character(s)"
 | 
			
		||||
msgid_plural "should be %{count} character(s)"
 | 
			
		||||
msgstr[0] ""
 | 
			
		||||
msgstr[1] ""
 | 
			
		||||
 | 
			
		||||
msgid "should have %{count} item(s)"
 | 
			
		||||
msgid_plural "should have %{count} item(s)"
 | 
			
		||||
msgstr[0] ""
 | 
			
		||||
msgstr[1] ""
 | 
			
		||||
 | 
			
		||||
msgid "should be at least %{count} character(s)"
 | 
			
		||||
msgid_plural "should be at least %{count} character(s)"
 | 
			
		||||
msgstr[0] ""
 | 
			
		||||
msgstr[1] ""
 | 
			
		||||
 | 
			
		||||
msgid "should have at least %{count} item(s)"
 | 
			
		||||
msgid_plural "should have at least %{count} item(s)"
 | 
			
		||||
msgstr[0] ""
 | 
			
		||||
msgstr[1] ""
 | 
			
		||||
 | 
			
		||||
msgid "should be at most %{count} character(s)"
 | 
			
		||||
msgid_plural "should be at most %{count} character(s)"
 | 
			
		||||
msgstr[0] ""
 | 
			
		||||
msgstr[1] ""
 | 
			
		||||
 | 
			
		||||
msgid "should have at most %{count} item(s)"
 | 
			
		||||
msgid_plural "should have at most %{count} item(s)"
 | 
			
		||||
msgstr[0] ""
 | 
			
		||||
msgstr[1] ""
 | 
			
		||||
 | 
			
		||||
msgid "must be less than %{number}"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "must be greater than %{number}"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "must be less than or equal to %{number}"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "must be greater than or equal to %{number}"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "must be equal to %{number}"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api.ex:523
 | 
			
		||||
msgid "Account not found"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api.ex:316
 | 
			
		||||
msgid "Already voted"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/o_auth/o_auth_controller.ex:402
 | 
			
		||||
msgid "Bad request"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/controller_helper.ex:97
 | 
			
		||||
#: lib/pleroma/web/controller_helper.ex:103
 | 
			
		||||
msgid "Can't display this activity"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:324
 | 
			
		||||
msgid "Can't find user"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:80
 | 
			
		||||
msgid "Can't get favorites"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api/utils.ex:482
 | 
			
		||||
msgid "Cannot post an empty status without attachments"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api/utils.ex:441
 | 
			
		||||
msgid "Comment must be up to %{max_size} characters"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/config_db.ex:200
 | 
			
		||||
msgid "Config with params %{params} not found"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api.ex:167 lib/pleroma/web/common_api.ex:171
 | 
			
		||||
msgid "Could not delete"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api.ex:217
 | 
			
		||||
msgid "Could not favorite"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api.ex:254
 | 
			
		||||
msgid "Could not unfavorite"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api.ex:202
 | 
			
		||||
msgid "Could not unrepeat"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api.ex:530 lib/pleroma/web/common_api.ex:539
 | 
			
		||||
msgid "Could not update state"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:205
 | 
			
		||||
msgid "Error."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/twitter_api/twitter_api.ex:99
 | 
			
		||||
msgid "Invalid CAPTCHA"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:144
 | 
			
		||||
#: lib/pleroma/web/o_auth/o_auth_controller.ex:631
 | 
			
		||||
msgid "Invalid credentials"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/plugs/ensure_authenticated_plug.ex:42
 | 
			
		||||
msgid "Invalid credentials."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api.ex:337
 | 
			
		||||
msgid "Invalid indices"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:29
 | 
			
		||||
msgid "Invalid parameters"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api/utils.ex:349
 | 
			
		||||
msgid "Invalid password."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:254
 | 
			
		||||
msgid "Invalid request"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/twitter_api/twitter_api.ex:102
 | 
			
		||||
msgid "Kocaptcha service unavailable"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:140
 | 
			
		||||
msgid "Missing parameters"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api/utils.ex:477
 | 
			
		||||
msgid "No such conversation"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:171
 | 
			
		||||
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:197 lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:239
 | 
			
		||||
msgid "No such permission_group"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:504
 | 
			
		||||
#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:11 lib/pleroma/web/feed/tag_controller.ex:16
 | 
			
		||||
#: lib/pleroma/web/feed/user_controller.ex:69 lib/pleroma/web/o_status/o_status_controller.ex:132
 | 
			
		||||
#: lib/pleroma/web/plugs/uploaded_media.ex:84
 | 
			
		||||
msgid "Not found"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api.ex:308
 | 
			
		||||
msgid "Poll's author can't vote"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:20
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:39 lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:51
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:52 lib/pleroma/web/mastodon_api/controllers/status_controller.ex:326
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:71
 | 
			
		||||
msgid "Record not found"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:35
 | 
			
		||||
#: lib/pleroma/web/feed/user_controller.ex:78 lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:42
 | 
			
		||||
#: lib/pleroma/web/o_status/o_status_controller.ex:138
 | 
			
		||||
msgid "Something went wrong"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api/activity_draft.ex:143
 | 
			
		||||
msgid "The message visibility must be direct"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api/utils.ex:492
 | 
			
		||||
msgid "The status is over the character limit"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/plugs/ensure_public_or_authenticated_plug.ex:36
 | 
			
		||||
msgid "This resource requires authentication."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/plugs/rate_limiter.ex:208
 | 
			
		||||
msgid "Throttled"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api.ex:338
 | 
			
		||||
msgid "Too many choices"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:268
 | 
			
		||||
msgid "You can't revoke your own admin status."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/o_auth/o_auth_controller.ex:243
 | 
			
		||||
#: lib/pleroma/web/o_auth/o_auth_controller.ex:333
 | 
			
		||||
msgid "Your account is currently disabled"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/o_auth/o_auth_controller.ex:205
 | 
			
		||||
#: lib/pleroma/web/o_auth/o_auth_controller.ex:356
 | 
			
		||||
msgid "Your login is missing a confirmed e-mail address"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:392
 | 
			
		||||
msgid "can't read inbox of %{nickname} as %{as_nickname}"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:491
 | 
			
		||||
msgid "can't update outbox of %{nickname} as %{as_nickname}"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api.ex:475
 | 
			
		||||
msgid "conversation is already muted"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:510
 | 
			
		||||
msgid "error"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:34
 | 
			
		||||
msgid "mascots can only be images"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:63
 | 
			
		||||
msgid "not found"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/o_auth/o_auth_controller.ex:437
 | 
			
		||||
msgid "Bad OAuth request."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/twitter_api/twitter_api.ex:108
 | 
			
		||||
msgid "CAPTCHA already used"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/twitter_api/twitter_api.ex:105
 | 
			
		||||
msgid "CAPTCHA expired"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/plugs/uploaded_media.ex:57
 | 
			
		||||
msgid "Failed"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/o_auth/o_auth_controller.ex:453
 | 
			
		||||
msgid "Failed to authenticate: %{message}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/o_auth/o_auth_controller.ex:484
 | 
			
		||||
msgid "Failed to set up user account."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/plugs/o_auth_scopes_plug.ex:37
 | 
			
		||||
msgid "Insufficient permissions: %{permissions}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/plugs/uploaded_media.ex:111
 | 
			
		||||
msgid "Internal Error"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/o_auth/fallback_controller.ex:22
 | 
			
		||||
#: lib/pleroma/web/o_auth/fallback_controller.ex:29
 | 
			
		||||
msgid "Invalid Username/Password"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/twitter_api/twitter_api.ex:111
 | 
			
		||||
msgid "Invalid answer data"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:33
 | 
			
		||||
msgid "Nodeinfo schema version not handled"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/o_auth/o_auth_controller.ex:194
 | 
			
		||||
msgid "This action is outside the authorized scopes"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/o_auth/fallback_controller.ex:14
 | 
			
		||||
msgid "Unknown error, please check the details and try again."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/o_auth/o_auth_controller.ex:136
 | 
			
		||||
#: lib/pleroma/web/o_auth/o_auth_controller.ex:180
 | 
			
		||||
msgid "Unlisted redirect_uri."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/o_auth/o_auth_controller.ex:433
 | 
			
		||||
msgid "Unsupported OAuth provider: %{provider}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/uploaders/uploader.ex:74
 | 
			
		||||
msgid "Uploader callback timeout"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/uploader_controller.ex:23
 | 
			
		||||
msgid "bad request"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/twitter_api/twitter_api.ex:96
 | 
			
		||||
msgid "CAPTCHA Error"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api.ex:266
 | 
			
		||||
msgid "Could not add reaction emoji"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api.ex:277
 | 
			
		||||
msgid "Could not remove reaction emoji"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/twitter_api/twitter_api.ex:122
 | 
			
		||||
msgid "Invalid CAPTCHA (Missing parameter: %{name})"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:96
 | 
			
		||||
msgid "List not found"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:151
 | 
			
		||||
msgid "Missing parameter: %{name}"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/o_auth/o_auth_controller.ex:232
 | 
			
		||||
#: lib/pleroma/web/o_auth/o_auth_controller.ex:346
 | 
			
		||||
msgid "Password reset is required"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/tests/auth_test_controller.ex:9
 | 
			
		||||
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:6 lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/admin_api/controllers/chat_controller.ex:6 lib/pleroma/web/admin_api/controllers/config_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:6 lib/pleroma/web/admin_api/controllers/frontend_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/admin_api/controllers/instance_controller.ex:6 lib/pleroma/web/admin_api/controllers/instance_document_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/admin_api/controllers/invite_controller.ex:6 lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex:6 lib/pleroma/web/admin_api/controllers/relay_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/admin_api/controllers/report_controller.ex:6 lib/pleroma/web/admin_api/controllers/status_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/admin_api/controllers/user_controller.ex:6 lib/pleroma/web/controller_helper.ex:6 lib/pleroma/web/embed_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/fallback/redirect_controller.ex:6 lib/pleroma/web/feed/tag_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/feed/user_controller.ex:6 lib/pleroma/web/mailer/subscription_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/manifest_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/account_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/app_controller.ex:11 lib/pleroma/web/mastodon_api/controllers/auth_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/directory_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/filter_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/instance_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/marker_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex:14 lib/pleroma/web/mastodon_api/controllers/media_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/notification_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/report_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/search_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/status_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:7 lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:6 lib/pleroma/web/media_proxy/media_proxy_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/mongoose_im/mongoose_im_controller.ex:6 lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/o_auth/fallback_controller.ex:6 lib/pleroma/web/o_auth/mfa_controller.ex:10
 | 
			
		||||
#: lib/pleroma/web/o_auth/o_auth_controller.ex:6 lib/pleroma/web/o_status/o_status_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/app_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/pleroma_api/controllers/backup_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/chat_controller.ex:5
 | 
			
		||||
#: lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/pleroma_api/controllers/instances_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/pleroma_api/controllers/notification_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/report_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex:7 lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/static_fe/static_fe_controller.ex:6 lib/pleroma/web/twitter_api/controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/twitter_api/controllers/password_controller.ex:10 lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:6 lib/pleroma/web/uploader_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/web_finger/web_finger_controller.ex:6
 | 
			
		||||
msgid "Security violation: OAuth scopes check was neither handled nor explicitly skipped."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/plugs/ensure_authenticated_plug.ex:32
 | 
			
		||||
msgid "Two-factor authentication enabled, you must use a access token."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61
 | 
			
		||||
msgid "Web push subscription is disabled on this Pleroma instance"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:234
 | 
			
		||||
msgid "You can't revoke your own admin/moderator status."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:129
 | 
			
		||||
msgid "authorization required for timeline view"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:24
 | 
			
		||||
msgid "Access denied"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:321
 | 
			
		||||
msgid "This API requires an authenticated user"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/plugs/ensure_staff_privileged_plug.ex:26
 | 
			
		||||
#: lib/pleroma/web/plugs/user_is_admin_plug.ex:21
 | 
			
		||||
msgid "User is not an admin."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/user/backup.ex:75
 | 
			
		||||
msgid "Last export was less than a day ago"
 | 
			
		||||
msgid_plural "Last export was less than %{days} days ago"
 | 
			
		||||
msgstr[0] ""
 | 
			
		||||
msgstr[1] ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/user/backup.ex:93
 | 
			
		||||
msgid "Backups require enabled email"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:423
 | 
			
		||||
msgid "Character limit (%{limit} characters) exceeded, contains %{length} characters"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/user/backup.ex:98
 | 
			
		||||
msgid "Email is required"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api/utils.ex:507
 | 
			
		||||
msgid "Too many attachments"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/plugs/ensure_staff_privileged_plug.ex:33
 | 
			
		||||
#: lib/pleroma/web/plugs/user_is_staff_plug.ex:20
 | 
			
		||||
msgid "User is not a staff member."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/o_auth/o_auth_controller.ex:366
 | 
			
		||||
msgid "Your account is awaiting approval."
 | 
			
		||||
msgstr ""
 | 
			
		||||
							
								
								
									
										153
									
								
								priv/gettext/en_test/LC_MESSAGES/posix_errors.po
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								priv/gettext/en_test/LC_MESSAGES/posix_errors.po
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,153 @@
 | 
			
		|||
## "msgid"s in this file come from POT (.pot) files.
 | 
			
		||||
##
 | 
			
		||||
## Do not add, change, or remove "msgid"s manually here as
 | 
			
		||||
## they're tied to the ones in the corresponding POT file
 | 
			
		||||
## (with the same domain).
 | 
			
		||||
##
 | 
			
		||||
## Use "mix gettext.extract --merge" or "mix gettext.merge"
 | 
			
		||||
## to merge POT files into PO files.
 | 
			
		||||
msgid ""
 | 
			
		||||
msgstr ""
 | 
			
		||||
"Language: en_test\n"
 | 
			
		||||
"Plural-Forms: nplurals=2\n"
 | 
			
		||||
 | 
			
		||||
msgid "eperm"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "eacces"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "eagain"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "ebadf"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "ebadmsg"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "ebusy"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "edeadlk"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "edeadlock"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "edquot"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "eexist"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "efault"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "efbig"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "eftype"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "eintr"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "einval"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "eio"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "eisdir"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "eloop"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "emfile"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "emlink"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "emultihop"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "enametoolong"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "enfile"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "enobufs"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "enodev"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "enolck"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "enolink"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "enoent"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "enomem"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "enospc"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "enosr"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "enostr"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "enosys"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "enotblk"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "enotdir"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "enotsup"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "enxio"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "eopnotsupp"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "eoverflow"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "epipe"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "erange"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "erofs"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "espipe"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "esrch"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "estale"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "etxtbsy"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "exdev"
 | 
			
		||||
msgstr ""
 | 
			
		||||
							
								
								
									
										529
									
								
								priv/gettext/en_test/LC_MESSAGES/static_pages.po
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										529
									
								
								priv/gettext/en_test/LC_MESSAGES/static_pages.po
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,529 @@
 | 
			
		|||
# SOME DESCRIPTIVE TITLE.
 | 
			
		||||
# Copyright (C) YEAR Free Software Foundation, Inc.
 | 
			
		||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
 | 
			
		||||
#
 | 
			
		||||
#, fuzzy
 | 
			
		||||
msgid ""
 | 
			
		||||
msgstr ""
 | 
			
		||||
"Project-Id-Version: PACKAGE VERSION\n"
 | 
			
		||||
"PO-Revision-Date: 2022-03-06 11:27-0500\n"
 | 
			
		||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 | 
			
		||||
"Language-Team: LANGUAGE <LL@li.org>\n"
 | 
			
		||||
"MIME-Version: 1.0\n"
 | 
			
		||||
"Content-Type: text/plain; charset=CHARSET\n"
 | 
			
		||||
"Content-Transfer-Encoding: 8bit\n"
 | 
			
		||||
 | 
			
		||||
#~ ## "msgid"s in this file come from POT (.pot) files.
 | 
			
		||||
#~ ##
 | 
			
		||||
#~ ## Do not add, change, or remove "msgid"s manually here as
 | 
			
		||||
#~ ## they're tied to the ones in the corresponding POT file
 | 
			
		||||
#~ ## (with the same domain).
 | 
			
		||||
#~ ##
 | 
			
		||||
#~ ## Use "mix gettext.extract --merge" or "mix gettext.merge"
 | 
			
		||||
#~ ## to merge POT files into PO files.
 | 
			
		||||
#~ msgid ""
 | 
			
		||||
#~ msgstr ""
 | 
			
		||||
#~ "Language: en_test\n"
 | 
			
		||||
#~ "Plural-Forms: nplurals=2\n"
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:9
 | 
			
		||||
msgctxt "remote follow authorization button"
 | 
			
		||||
msgid "Authorize"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:2
 | 
			
		||||
msgctxt "remote follow error"
 | 
			
		||||
msgid "Error fetching user"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:4
 | 
			
		||||
msgctxt "remote follow header"
 | 
			
		||||
msgid "Remote follow"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:8
 | 
			
		||||
msgctxt "placeholder text for auth code entry"
 | 
			
		||||
msgid "Authentication code"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:10
 | 
			
		||||
msgctxt "placeholder text for password entry"
 | 
			
		||||
msgid "Password"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:8
 | 
			
		||||
msgctxt "placeholder text for username entry"
 | 
			
		||||
msgid "Username"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:13
 | 
			
		||||
msgctxt "remote follow authorization button for login"
 | 
			
		||||
msgid "Authorize"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:12
 | 
			
		||||
msgctxt "remote follow authorization button for mfa"
 | 
			
		||||
msgid "Authorize"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex:2
 | 
			
		||||
msgctxt "remote follow error"
 | 
			
		||||
msgid "Error following account"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:4
 | 
			
		||||
msgctxt "remote follow header, need login"
 | 
			
		||||
msgid "Log in to follow"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:4
 | 
			
		||||
msgctxt "remote follow mfa header"
 | 
			
		||||
msgid "Two-factor authentication"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex:4
 | 
			
		||||
msgctxt "remote follow success"
 | 
			
		||||
msgid "Account followed!"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:7
 | 
			
		||||
msgctxt "placeholder text for account id"
 | 
			
		||||
msgid "Your account ID, e.g. lain@quitter.se"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:8
 | 
			
		||||
msgctxt "remote follow authorization button for following with a remote account"
 | 
			
		||||
msgid "Follow"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:2
 | 
			
		||||
msgctxt "remote follow error"
 | 
			
		||||
msgid "Error: %{error}"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:4
 | 
			
		||||
msgctxt "remote follow header"
 | 
			
		||||
msgid "Remotely follow %{nickname}"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:12
 | 
			
		||||
msgctxt "password reset button"
 | 
			
		||||
msgid "Reset"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex:4
 | 
			
		||||
msgctxt "password reset failed homepage link"
 | 
			
		||||
msgid "Homepage"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex:1
 | 
			
		||||
msgctxt "password reset failed message"
 | 
			
		||||
msgid "Password reset failed"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:8
 | 
			
		||||
msgctxt "password reset form confirm password prompt"
 | 
			
		||||
msgid "Confirmation"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:4
 | 
			
		||||
msgctxt "password reset form password prompt"
 | 
			
		||||
msgid "Password"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/password/invalid_token.html.eex:1
 | 
			
		||||
msgctxt "password reset invalid token message"
 | 
			
		||||
msgid "Invalid Token"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex:2
 | 
			
		||||
msgctxt "password reset successful homepage link"
 | 
			
		||||
msgid "Homepage"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex:1
 | 
			
		||||
msgctxt "password reset successful message"
 | 
			
		||||
msgid "Password changed!"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/feed/feed/tag.atom.eex:15
 | 
			
		||||
#: lib/pleroma/web/templates/feed/feed/tag.rss.eex:7
 | 
			
		||||
msgctxt "tag feed description"
 | 
			
		||||
msgid "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex:1
 | 
			
		||||
msgctxt "oauth authorization exists page title"
 | 
			
		||||
msgid "Authorization exists"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:32
 | 
			
		||||
msgctxt "oauth authorize approve button"
 | 
			
		||||
msgid "Approve"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:30
 | 
			
		||||
msgctxt "oauth authorize cancel button"
 | 
			
		||||
msgid "Cancel"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:23
 | 
			
		||||
msgctxt "oauth authorize message"
 | 
			
		||||
msgid "Application <strong>%{client_name}</strong> is requesting access to your account."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex:1
 | 
			
		||||
msgctxt "oauth authorized page title"
 | 
			
		||||
msgid "Successfully authorized"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex:1
 | 
			
		||||
msgctxt "oauth external provider page title"
 | 
			
		||||
msgid "Sign in with external provider"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex:13
 | 
			
		||||
msgctxt "oauth external provider sign in button"
 | 
			
		||||
msgid "Sign in with %{strategy}"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:54
 | 
			
		||||
msgctxt "oauth login button"
 | 
			
		||||
msgid "Log In"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:51
 | 
			
		||||
msgctxt "oauth login password prompt"
 | 
			
		||||
msgid "Password"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:47
 | 
			
		||||
msgctxt "oauth login username prompt"
 | 
			
		||||
msgid "Username"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:39
 | 
			
		||||
msgctxt "oauth register nickname prompt"
 | 
			
		||||
msgid "Pleroma Handle"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:37
 | 
			
		||||
msgctxt "oauth register nickname unchangeable warning"
 | 
			
		||||
msgid "Choose carefully! You won't be able to change this later. You will be able to change your display name, though."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:18
 | 
			
		||||
msgctxt "oauth register page email prompt"
 | 
			
		||||
msgid "Email"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:10
 | 
			
		||||
msgctxt "oauth register page fill form prompt"
 | 
			
		||||
msgid "If you'd like to register a new account, please provide the details below."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:35
 | 
			
		||||
msgctxt "oauth register page login button"
 | 
			
		||||
msgid "Proceed as existing user"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:31
 | 
			
		||||
msgctxt "oauth register page login password prompt"
 | 
			
		||||
msgid "Password"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:24
 | 
			
		||||
msgctxt "oauth register page login prompt"
 | 
			
		||||
msgid "Alternatively, sign in to connect to existing account."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:27
 | 
			
		||||
msgctxt "oauth register page login username prompt"
 | 
			
		||||
msgid "Name or email"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:14
 | 
			
		||||
msgctxt "oauth register page nickname prompt"
 | 
			
		||||
msgid "Nickname"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:22
 | 
			
		||||
msgctxt "oauth register page register button"
 | 
			
		||||
msgid "Proceed as new user"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:8
 | 
			
		||||
msgctxt "oauth register page title"
 | 
			
		||||
msgid "Registration Details"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:36
 | 
			
		||||
msgctxt "oauth register page title"
 | 
			
		||||
msgid "This is the first time you visit! Please enter your Pleroma handle."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex:2
 | 
			
		||||
msgctxt "oauth scopes message"
 | 
			
		||||
msgid "The following permissions will be granted"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex:2
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex:2
 | 
			
		||||
msgctxt "oauth token code message"
 | 
			
		||||
msgid "Token code is <br>%{token}"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:12
 | 
			
		||||
msgctxt "mfa auth code prompt"
 | 
			
		||||
msgid "Authentication code"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:8
 | 
			
		||||
msgctxt "mfa auth page title"
 | 
			
		||||
msgid "Two-factor authentication"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:23
 | 
			
		||||
msgctxt "mfa auth page use recovery code link"
 | 
			
		||||
msgid "Enter a two-factor recovery code"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:20
 | 
			
		||||
msgctxt "mfa auth verify code button"
 | 
			
		||||
msgid "Verify"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:8
 | 
			
		||||
msgctxt "mfa recover page title"
 | 
			
		||||
msgid "Two-factor recovery"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:12
 | 
			
		||||
msgctxt "mfa recover recovery code prompt"
 | 
			
		||||
msgid "Recovery code"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:23
 | 
			
		||||
msgctxt "mfa recover use 2fa code link"
 | 
			
		||||
msgid "Enter a two-factor code"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:20
 | 
			
		||||
msgctxt "mfa recover verify recovery code button"
 | 
			
		||||
msgid "Verify"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex:8
 | 
			
		||||
msgctxt "static fe profile page remote follow button"
 | 
			
		||||
msgid "Remote follow"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/email/digest.html.eex:163
 | 
			
		||||
msgctxt "digest email header line"
 | 
			
		||||
msgid "Hey %{nickname}, here is what you've missed!"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/email/digest.html.eex:544
 | 
			
		||||
msgctxt "digest email receiver address"
 | 
			
		||||
msgid "The email address you are subscribed as is <a href='mailto:%{@user.email}' style='color: %{color};text-decoration: none;'>%{email}</a>. "
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/email/digest.html.eex:538
 | 
			
		||||
msgctxt "digest email sending reason"
 | 
			
		||||
msgid "You have received this email because you have signed up to receive digest emails from <b>%{instance}</b> Pleroma instance."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/email/digest.html.eex:547
 | 
			
		||||
msgctxt "digest email unsubscribe action"
 | 
			
		||||
msgid "To unsubscribe, please go %{here}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/email/digest.html.eex:547
 | 
			
		||||
msgctxt "digest email unsubscribe action link text"
 | 
			
		||||
msgid "here"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/mailer/subscription/unsubscribe_failure.html.eex:1
 | 
			
		||||
msgctxt "mailer unsubscribe failed message"
 | 
			
		||||
msgid "UNSUBSCRIBE FAILURE"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/mailer/subscription/unsubscribe_success.html.eex:1
 | 
			
		||||
msgctxt "mailer unsubscribe successful message"
 | 
			
		||||
msgid "UNSUBSCRIBE SUCCESSFUL"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/email/digest.html.eex:385
 | 
			
		||||
msgctxt "new followers count header"
 | 
			
		||||
msgid "%{count} New Follower"
 | 
			
		||||
msgid_plural "%{count} New Followers"
 | 
			
		||||
msgstr[0] "xx%{count} New Followerxx"
 | 
			
		||||
msgstr[1] "xx%{count} New Followersxx"
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/emails/user_email.ex:356
 | 
			
		||||
msgctxt "account archive email body - self-requested"
 | 
			
		||||
msgid "<p>You requested a full backup of your Pleroma account. It's ready for download:</p>\n<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/emails/user_email.ex:384
 | 
			
		||||
msgctxt "account archive email subject"
 | 
			
		||||
msgid "Your account archive is ready"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/emails/user_email.ex:188
 | 
			
		||||
msgctxt "approval pending email body"
 | 
			
		||||
msgid "<h3>Awaiting Approval</h3>\n<p>Your account at %{instance_name} is being reviewed by staff. You will receive another email once your account is approved.</p>\n"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/emails/user_email.ex:202
 | 
			
		||||
msgctxt "approval pending email subject"
 | 
			
		||||
msgid "Your account is awaiting approval"
 | 
			
		||||
msgstr "xxYour account is awaiting approvalxx"
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/emails/user_email.ex:158
 | 
			
		||||
msgctxt "confirmation email body"
 | 
			
		||||
msgid "<h3>Thank you for registering on %{instance_name}</h3>\n<p>Email confirmation is required to activate the account.</p>\n<p>Please click the following link to <a href=\"%{confirmation_url}\">activate your account</a>.</p>\n"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/emails/user_email.ex:174
 | 
			
		||||
msgctxt "confirmation email subject"
 | 
			
		||||
msgid "%{instance_name} account confirmation"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/emails/user_email.ex:310
 | 
			
		||||
msgctxt "digest email subject"
 | 
			
		||||
msgid "Your digest from %{instance_name}"
 | 
			
		||||
msgstr "xxYour digest from %{instance_name}xx"
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/emails/user_email.ex:81
 | 
			
		||||
msgctxt "password reset email body"
 | 
			
		||||
msgid "<h3>Reset your password at %{instance_name}</h3>\n<p>Someone has requested password change for your account at %{instance_name}.</p>\n<p>If it was you, visit the following link to proceed: <a href=\"%{password_reset_url}\">reset password</a>.</p>\n<p>If it was someone else, nothing to worry about: your data is secure and your password has not been changed.</p>\n"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/emails/user_email.ex:98
 | 
			
		||||
msgctxt "password reset email subject"
 | 
			
		||||
msgid "Password reset"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/emails/user_email.ex:215
 | 
			
		||||
msgctxt "successful registration email body"
 | 
			
		||||
msgid "<h3>Hello @%{nickname},</h3>\n<p>Your account at %{instance_name} has been registered successfully.</p>\n<p>No further action is required to activate your account.</p>\n"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/emails/user_email.ex:231
 | 
			
		||||
msgctxt "successful registration email subject"
 | 
			
		||||
msgid "Account registered on %{instance_name}"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/emails/user_email.ex:119
 | 
			
		||||
msgctxt "user invitation email body"
 | 
			
		||||
msgid "<h3>You are invited to %{instance_name}</h3>\n<p>%{inviter_name} invites you to join %{instance_name}, an instance of Pleroma federated social networking platform.</p>\n<p>Click the following link to register: <a href=\"%{registration_url}\">accept invitation</a>.</p>\n"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/emails/user_email.ex:136
 | 
			
		||||
msgctxt "user invitation email subject"
 | 
			
		||||
msgid "Invitation to %{instance_name}"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/emails/user_email.ex:53
 | 
			
		||||
msgctxt "welcome email html body"
 | 
			
		||||
msgid "Welcome to %{instance_name}!"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/emails/user_email.ex:41
 | 
			
		||||
msgctxt "welcome email subject"
 | 
			
		||||
msgid "Welcome to %{instance_name}!"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/emails/user_email.ex:65
 | 
			
		||||
msgctxt "welcome email text body"
 | 
			
		||||
msgid "Welcome to %{instance_name}!"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/emails/user_email.ex:368
 | 
			
		||||
msgctxt "account archive email body - admin requested"
 | 
			
		||||
msgid "<p>Admin @%{admin_nickname} requested a full backup of your Pleroma account. It's ready for download:</p>\n<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
 | 
			
		||||
msgstr ""
 | 
			
		||||
| 
						 | 
				
			
			@ -90,121 +90,99 @@ msgid "must be equal to %{number}"
 | 
			
		|||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api/common_api.ex:505
 | 
			
		||||
#: lib/pleroma/web/common_api.ex:523
 | 
			
		||||
msgid "Account not found"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api/common_api.ex:339
 | 
			
		||||
#: lib/pleroma/web/common_api.ex:316
 | 
			
		||||
msgid "Already voted"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/oauth/oauth_controller.ex:359
 | 
			
		||||
#: lib/pleroma/web/o_auth/o_auth_controller.ex:402
 | 
			
		||||
msgid "Bad request"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:426
 | 
			
		||||
msgid "Can't delete object"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/controller_helper.ex:105
 | 
			
		||||
#: lib/pleroma/web/controller_helper.ex:111
 | 
			
		||||
#: lib/pleroma/web/controller_helper.ex:97
 | 
			
		||||
#: lib/pleroma/web/controller_helper.ex:103
 | 
			
		||||
msgid "Can't display this activity"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:285
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:324
 | 
			
		||||
msgid "Can't find user"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:61
 | 
			
		||||
#: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:80
 | 
			
		||||
msgid "Can't get favorites"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:438
 | 
			
		||||
msgid "Can't like object"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api/utils.ex:563
 | 
			
		||||
#: lib/pleroma/web/common_api/utils.ex:482
 | 
			
		||||
msgid "Cannot post an empty status without attachments"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api/utils.ex:511
 | 
			
		||||
#: lib/pleroma/web/common_api/utils.ex:441
 | 
			
		||||
msgid "Comment must be up to %{max_size} characters"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/config/config_db.ex:191
 | 
			
		||||
#: lib/pleroma/config_db.ex:200
 | 
			
		||||
msgid "Config with params %{params} not found"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api/common_api.ex:181
 | 
			
		||||
#: lib/pleroma/web/common_api/common_api.ex:185
 | 
			
		||||
#: lib/pleroma/web/common_api.ex:167 lib/pleroma/web/common_api.ex:171
 | 
			
		||||
msgid "Could not delete"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api/common_api.ex:231
 | 
			
		||||
#: lib/pleroma/web/common_api.ex:217
 | 
			
		||||
msgid "Could not favorite"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api/common_api.ex:453
 | 
			
		||||
msgid "Could not pin"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api/common_api.ex:278
 | 
			
		||||
#: lib/pleroma/web/common_api.ex:254
 | 
			
		||||
msgid "Could not unfavorite"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api/common_api.ex:463
 | 
			
		||||
msgid "Could not unpin"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api/common_api.ex:216
 | 
			
		||||
#: lib/pleroma/web/common_api.ex:202
 | 
			
		||||
msgid "Could not unrepeat"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api/common_api.ex:512
 | 
			
		||||
#: lib/pleroma/web/common_api/common_api.ex:521
 | 
			
		||||
#: lib/pleroma/web/common_api.ex:530 lib/pleroma/web/common_api.ex:539
 | 
			
		||||
msgid "Could not update state"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:207
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:205
 | 
			
		||||
msgid "Error."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/twitter_api/twitter_api.ex:106
 | 
			
		||||
#: lib/pleroma/web/twitter_api/twitter_api.ex:99
 | 
			
		||||
msgid "Invalid CAPTCHA"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:116
 | 
			
		||||
#: lib/pleroma/web/oauth/oauth_controller.ex:568
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:144
 | 
			
		||||
#: lib/pleroma/web/o_auth/o_auth_controller.ex:631
 | 
			
		||||
msgid "Invalid credentials"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/plugs/ensure_authenticated_plug.ex:38
 | 
			
		||||
#: lib/pleroma/web/plugs/ensure_authenticated_plug.ex:42
 | 
			
		||||
msgid "Invalid credentials."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api/common_api.ex:355
 | 
			
		||||
#: lib/pleroma/web/common_api.ex:337
 | 
			
		||||
msgid "Invalid indices"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -214,189 +192,184 @@ msgid "Invalid parameters"
 | 
			
		|||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api/utils.ex:414
 | 
			
		||||
#: lib/pleroma/web/common_api/utils.ex:349
 | 
			
		||||
msgid "Invalid password."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:220
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:254
 | 
			
		||||
msgid "Invalid request"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/twitter_api/twitter_api.ex:109
 | 
			
		||||
#: lib/pleroma/web/twitter_api/twitter_api.ex:102
 | 
			
		||||
msgid "Kocaptcha service unavailable"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:112
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:140
 | 
			
		||||
msgid "Missing parameters"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api/utils.ex:547
 | 
			
		||||
#: lib/pleroma/web/common_api/utils.ex:477
 | 
			
		||||
msgid "No such conversation"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:388
 | 
			
		||||
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:414 lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:456
 | 
			
		||||
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:171
 | 
			
		||||
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:197 lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:239
 | 
			
		||||
msgid "No such permission_group"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/plugs/uploaded_media.ex:84
 | 
			
		||||
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:486 lib/pleroma/web/admin_api/controllers/fallback_controller.ex:11
 | 
			
		||||
#: lib/pleroma/web/feed/user_controller.ex:71 lib/pleroma/web/ostatus/ostatus_controller.ex:143
 | 
			
		||||
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:504
 | 
			
		||||
#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:11 lib/pleroma/web/feed/tag_controller.ex:16
 | 
			
		||||
#: lib/pleroma/web/feed/user_controller.ex:69 lib/pleroma/web/o_status/o_status_controller.ex:132
 | 
			
		||||
#: lib/pleroma/web/plugs/uploaded_media.ex:84
 | 
			
		||||
msgid "Not found"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api/common_api.ex:331
 | 
			
		||||
#: lib/pleroma/web/common_api.ex:308
 | 
			
		||||
msgid "Poll's author can't vote"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:20
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:37 lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:49
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:50 lib/pleroma/web/mastodon_api/controllers/status_controller.ex:306
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:39 lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:51
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:52 lib/pleroma/web/mastodon_api/controllers/status_controller.ex:326
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:71
 | 
			
		||||
msgid "Record not found"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:35
 | 
			
		||||
#: lib/pleroma/web/feed/user_controller.ex:77 lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:36
 | 
			
		||||
#: lib/pleroma/web/ostatus/ostatus_controller.ex:149
 | 
			
		||||
#: lib/pleroma/web/feed/user_controller.ex:78 lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:42
 | 
			
		||||
#: lib/pleroma/web/o_status/o_status_controller.ex:138
 | 
			
		||||
msgid "Something went wrong"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api/activity_draft.ex:107
 | 
			
		||||
#: lib/pleroma/web/common_api/activity_draft.ex:143
 | 
			
		||||
msgid "The message visibility must be direct"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api/utils.ex:573
 | 
			
		||||
#: lib/pleroma/web/common_api/utils.ex:492
 | 
			
		||||
msgid "The status is over the character limit"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex:31
 | 
			
		||||
#: lib/pleroma/web/plugs/ensure_public_or_authenticated_plug.ex:36
 | 
			
		||||
msgid "This resource requires authentication."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/plugs/rate_limiter/rate_limiter.ex:206
 | 
			
		||||
#: lib/pleroma/web/plugs/rate_limiter.ex:208
 | 
			
		||||
msgid "Throttled"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api/common_api.ex:356
 | 
			
		||||
#: lib/pleroma/web/common_api.ex:338
 | 
			
		||||
msgid "Too many choices"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:443
 | 
			
		||||
msgid "Unhandled activity type"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:485
 | 
			
		||||
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:268
 | 
			
		||||
msgid "You can't revoke your own admin status."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/oauth/oauth_controller.ex:221
 | 
			
		||||
#: lib/pleroma/web/oauth/oauth_controller.ex:308
 | 
			
		||||
#: lib/pleroma/web/o_auth/o_auth_controller.ex:243
 | 
			
		||||
#: lib/pleroma/web/o_auth/o_auth_controller.ex:333
 | 
			
		||||
msgid "Your account is currently disabled"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/oauth/oauth_controller.ex:183
 | 
			
		||||
#: lib/pleroma/web/oauth/oauth_controller.ex:331
 | 
			
		||||
#: lib/pleroma/web/o_auth/o_auth_controller.ex:205
 | 
			
		||||
#: lib/pleroma/web/o_auth/o_auth_controller.ex:356
 | 
			
		||||
msgid "Your login is missing a confirmed e-mail address"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:390
 | 
			
		||||
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:392
 | 
			
		||||
msgid "can't read inbox of %{nickname} as %{as_nickname}"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:473
 | 
			
		||||
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:491
 | 
			
		||||
msgid "can't update outbox of %{nickname} as %{as_nickname}"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api/common_api.ex:471
 | 
			
		||||
#: lib/pleroma/web/common_api.ex:475
 | 
			
		||||
msgid "conversation is already muted"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:314
 | 
			
		||||
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:492
 | 
			
		||||
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:510
 | 
			
		||||
msgid "error"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:32
 | 
			
		||||
#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:34
 | 
			
		||||
msgid "mascots can only be images"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:62
 | 
			
		||||
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:63
 | 
			
		||||
msgid "not found"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/oauth/oauth_controller.ex:394
 | 
			
		||||
#: lib/pleroma/web/o_auth/o_auth_controller.ex:437
 | 
			
		||||
msgid "Bad OAuth request."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/twitter_api/twitter_api.ex:115
 | 
			
		||||
#: lib/pleroma/web/twitter_api/twitter_api.ex:108
 | 
			
		||||
msgid "CAPTCHA already used"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/twitter_api/twitter_api.ex:112
 | 
			
		||||
#: lib/pleroma/web/twitter_api/twitter_api.ex:105
 | 
			
		||||
msgid "CAPTCHA expired"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/plugs/uploaded_media.ex:57
 | 
			
		||||
#: lib/pleroma/web/plugs/uploaded_media.ex:57
 | 
			
		||||
msgid "Failed"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/oauth/oauth_controller.ex:410
 | 
			
		||||
#: lib/pleroma/web/o_auth/o_auth_controller.ex:453
 | 
			
		||||
msgid "Failed to authenticate: %{message}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/oauth/oauth_controller.ex:441
 | 
			
		||||
#: lib/pleroma/web/o_auth/o_auth_controller.ex:484
 | 
			
		||||
msgid "Failed to set up user account."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/plugs/oauth_scopes_plug.ex:38
 | 
			
		||||
#: lib/pleroma/web/plugs/o_auth_scopes_plug.ex:37
 | 
			
		||||
msgid "Insufficient permissions: %{permissions}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/plugs/uploaded_media.ex:104
 | 
			
		||||
#: lib/pleroma/web/plugs/uploaded_media.ex:111
 | 
			
		||||
msgid "Internal Error"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/oauth/fallback_controller.ex:22
 | 
			
		||||
#: lib/pleroma/web/oauth/fallback_controller.ex:29
 | 
			
		||||
#: lib/pleroma/web/o_auth/fallback_controller.ex:22
 | 
			
		||||
#: lib/pleroma/web/o_auth/fallback_controller.ex:29
 | 
			
		||||
msgid "Invalid Username/Password"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/twitter_api/twitter_api.ex:118
 | 
			
		||||
#: lib/pleroma/web/twitter_api/twitter_api.ex:111
 | 
			
		||||
msgid "Invalid answer data"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -406,28 +379,28 @@ msgid "Nodeinfo schema version not handled"
 | 
			
		|||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/oauth/oauth_controller.ex:172
 | 
			
		||||
#: lib/pleroma/web/o_auth/o_auth_controller.ex:194
 | 
			
		||||
msgid "This action is outside the authorized scopes"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/oauth/fallback_controller.ex:14
 | 
			
		||||
#: lib/pleroma/web/o_auth/fallback_controller.ex:14
 | 
			
		||||
msgid "Unknown error, please check the details and try again."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/oauth/oauth_controller.ex:119
 | 
			
		||||
#: lib/pleroma/web/oauth/oauth_controller.ex:158
 | 
			
		||||
#: lib/pleroma/web/o_auth/o_auth_controller.ex:136
 | 
			
		||||
#: lib/pleroma/web/o_auth/o_auth_controller.ex:180
 | 
			
		||||
msgid "Unlisted redirect_uri."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/oauth/oauth_controller.ex:390
 | 
			
		||||
#: lib/pleroma/web/o_auth/o_auth_controller.ex:433
 | 
			
		||||
msgid "Unsupported OAuth provider: %{provider}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/uploaders/uploader.ex:72
 | 
			
		||||
#: lib/pleroma/uploaders/uploader.ex:74
 | 
			
		||||
msgid "Uploader callback timeout"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -437,120 +410,101 @@ msgid "bad request"
 | 
			
		|||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/twitter_api/twitter_api.ex:103
 | 
			
		||||
#: lib/pleroma/web/twitter_api/twitter_api.ex:96
 | 
			
		||||
msgid "CAPTCHA Error"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api/common_api.ex:290
 | 
			
		||||
#: lib/pleroma/web/common_api.ex:266
 | 
			
		||||
msgid "Could not add reaction emoji"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api/common_api.ex:301
 | 
			
		||||
#: lib/pleroma/web/common_api.ex:277
 | 
			
		||||
msgid "Could not remove reaction emoji"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/twitter_api/twitter_api.ex:129
 | 
			
		||||
#: lib/pleroma/web/twitter_api/twitter_api.ex:122
 | 
			
		||||
msgid "Invalid CAPTCHA (Missing parameter: %{name})"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:92
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:96
 | 
			
		||||
msgid "List not found"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:123
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:151
 | 
			
		||||
msgid "Missing parameter: %{name}"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/oauth/oauth_controller.ex:210
 | 
			
		||||
#: lib/pleroma/web/oauth/oauth_controller.ex:321
 | 
			
		||||
#: lib/pleroma/web/o_auth/o_auth_controller.ex:232
 | 
			
		||||
#: lib/pleroma/web/o_auth/o_auth_controller.ex:346
 | 
			
		||||
msgid "Password reset is required"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/tests/auth_test_controller.ex:9
 | 
			
		||||
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:6 lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/admin_api/controllers/config_controller.ex:6 lib/pleroma/web/admin_api/controllers/fallback_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/admin_api/controllers/chat_controller.ex:6 lib/pleroma/web/admin_api/controllers/config_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:6 lib/pleroma/web/admin_api/controllers/frontend_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/admin_api/controllers/instance_controller.ex:6 lib/pleroma/web/admin_api/controllers/instance_document_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/admin_api/controllers/invite_controller.ex:6 lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex:6 lib/pleroma/web/admin_api/controllers/relay_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex:6 lib/pleroma/web/admin_api/controllers/relay_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/admin_api/controllers/report_controller.ex:6 lib/pleroma/web/admin_api/controllers/status_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/controller_helper.ex:6 lib/pleroma/web/embed_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/fallback_redirect_controller.ex:6 lib/pleroma/web/feed/tag_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/feed/user_controller.ex:6 lib/pleroma/web/mailer/subscription_controller.ex:2
 | 
			
		||||
#: lib/pleroma/web/masto_fe_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/account_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/app_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/auth_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/admin_api/controllers/user_controller.ex:6 lib/pleroma/web/controller_helper.ex:6 lib/pleroma/web/embed_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/fallback/redirect_controller.ex:6 lib/pleroma/web/feed/tag_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/feed/user_controller.ex:6 lib/pleroma/web/mailer/subscription_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/manifest_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/account_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/app_controller.ex:11 lib/pleroma/web/mastodon_api/controllers/auth_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/filter_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/instance_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/list_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/marker_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex:14
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/media_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/notification_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/report_controller.ex:8
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/search_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/status_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:7
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/media_proxy/media_proxy_controller.ex:6 lib/pleroma/web/mongooseim/mongoose_im_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:6 lib/pleroma/web/oauth/fallback_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/oauth/mfa_controller.ex:10 lib/pleroma/web/oauth/oauth_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/ostatus/ostatus_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/account_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/pleroma_api/controllers/chat_controller.ex:5 lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:2 lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/notification_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/directory_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/filter_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/instance_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/marker_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex:14 lib/pleroma/web/mastodon_api/controllers/media_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/notification_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/report_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/search_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/status_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:7 lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:6 lib/pleroma/web/media_proxy/media_proxy_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/mongoose_im/mongoose_im_controller.ex:6 lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/o_auth/fallback_controller.ex:6 lib/pleroma/web/o_auth/mfa_controller.ex:10
 | 
			
		||||
#: lib/pleroma/web/o_auth/o_auth_controller.ex:6 lib/pleroma/web/o_status/o_status_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/app_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/pleroma_api/controllers/backup_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/chat_controller.ex:5
 | 
			
		||||
#: lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/pleroma_api/controllers/instances_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/pleroma_api/controllers/notification_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/report_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex:7 lib/pleroma/web/static_fe/static_fe_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex:7 lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/static_fe/static_fe_controller.ex:6 lib/pleroma/web/twitter_api/controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/twitter_api/controllers/password_controller.ex:10 lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:6 lib/pleroma/web/twitter_api/twitter_api_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/uploader_controller.ex:6 lib/pleroma/web/web_finger/web_finger_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:6 lib/pleroma/web/uploader_controller.ex:6
 | 
			
		||||
#: lib/pleroma/web/web_finger/web_finger_controller.ex:6
 | 
			
		||||
msgid "Security violation: OAuth scopes check was neither handled nor explicitly skipped."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/plugs/ensure_authenticated_plug.ex:28
 | 
			
		||||
#: lib/pleroma/web/plugs/ensure_authenticated_plug.ex:32
 | 
			
		||||
msgid "Two-factor authentication enabled, you must use a access token."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:210
 | 
			
		||||
msgid "Unexpected error occurred while adding file to pack."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:138
 | 
			
		||||
msgid "Unexpected error occurred while creating pack."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:278
 | 
			
		||||
msgid "Unexpected error occurred while removing file from pack."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:250
 | 
			
		||||
msgid "Unexpected error occurred while updating file in pack."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:179
 | 
			
		||||
msgid "Unexpected error occurred while updating pack metadata."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61
 | 
			
		||||
msgid "Web push subscription is disabled on this Pleroma instance"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:451
 | 
			
		||||
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:234
 | 
			
		||||
msgid "You can't revoke your own admin/moderator status."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:126
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:129
 | 
			
		||||
msgid "authorization required for timeline view"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -560,11 +514,50 @@ msgid "Access denied"
 | 
			
		|||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:282
 | 
			
		||||
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:321
 | 
			
		||||
msgid "This API requires an authenticated user"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/plugs/user_is_admin_plug.ex:21
 | 
			
		||||
#: lib/pleroma/web/plugs/ensure_staff_privileged_plug.ex:26
 | 
			
		||||
#: lib/pleroma/web/plugs/user_is_admin_plug.ex:21
 | 
			
		||||
msgid "User is not an admin."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/user/backup.ex:75
 | 
			
		||||
msgid "Last export was less than a day ago"
 | 
			
		||||
msgid_plural "Last export was less than %{days} days ago"
 | 
			
		||||
msgstr[0] ""
 | 
			
		||||
msgstr[1] ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/user/backup.ex:93
 | 
			
		||||
msgid "Backups require enabled email"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:423
 | 
			
		||||
msgid "Character limit (%{limit} characters) exceeded, contains %{length} characters"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/user/backup.ex:98
 | 
			
		||||
msgid "Email is required"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/common_api/utils.ex:507
 | 
			
		||||
msgid "Too many attachments"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/plugs/ensure_staff_privileged_plug.ex:33
 | 
			
		||||
#: lib/pleroma/web/plugs/user_is_staff_plug.ex:20
 | 
			
		||||
msgid "User is not a staff member."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/o_auth/o_auth_controller.ex:366
 | 
			
		||||
msgid "Your account is awaiting approval."
 | 
			
		||||
msgstr ""
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,135 +15,135 @@ msgstr ""
 | 
			
		|||
 | 
			
		||||
msgid "eagain"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "ebadf"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "ebadmsg"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "ebusy"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "edeadlk"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "edeadlock"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "edquot"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "eexist"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "efault"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "efbig"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "eftype"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "eintr"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "einval"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "eio"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "eisdir"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "eloop"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "emfile"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "emlink"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "emultihop"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "enametoolong"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "enfile"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "enobufs"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "enodev"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "enolck"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "enolink"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "enoent"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "enomem"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "enospc"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "enosr"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "enostr"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "enosys"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "enotblk"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "enotdir"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "enotsup"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "enxio"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "eopnotsupp"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "eoverflow"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "epipe"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "erange"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "erofs"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "espipe"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "esrch"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "estale"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "etxtbsy"
 | 
			
		||||
msgstr ""
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
msgid "exdev"
 | 
			
		||||
msgstr ""
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										513
									
								
								priv/gettext/static_pages.pot
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										513
									
								
								priv/gettext/static_pages.pot
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,513 @@
 | 
			
		|||
## This file is a PO Template file.
 | 
			
		||||
##
 | 
			
		||||
## "msgid"s here are often extracted from source code.
 | 
			
		||||
## Add new translations manually only if they're dynamic
 | 
			
		||||
## translations that can't be statically extracted.
 | 
			
		||||
##
 | 
			
		||||
## Run "mix gettext.extract" to bring this file up to
 | 
			
		||||
## date. Leave "msgstr"s empty as changing them here as no
 | 
			
		||||
## effect: edit them in PO (.po) files instead.
 | 
			
		||||
msgid ""
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:9
 | 
			
		||||
msgctxt "remote follow authorization button"
 | 
			
		||||
msgid "Authorize"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:2
 | 
			
		||||
msgctxt "remote follow error"
 | 
			
		||||
msgid "Error fetching user"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:4
 | 
			
		||||
msgctxt "remote follow header"
 | 
			
		||||
msgid "Remote follow"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:8
 | 
			
		||||
msgctxt "placeholder text for auth code entry"
 | 
			
		||||
msgid "Authentication code"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:10
 | 
			
		||||
msgctxt "placeholder text for password entry"
 | 
			
		||||
msgid "Password"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:8
 | 
			
		||||
msgctxt "placeholder text for username entry"
 | 
			
		||||
msgid "Username"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:13
 | 
			
		||||
msgctxt "remote follow authorization button for login"
 | 
			
		||||
msgid "Authorize"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:12
 | 
			
		||||
msgctxt "remote follow authorization button for mfa"
 | 
			
		||||
msgid "Authorize"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex:2
 | 
			
		||||
msgctxt "remote follow error"
 | 
			
		||||
msgid "Error following account"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:4
 | 
			
		||||
msgctxt "remote follow header, need login"
 | 
			
		||||
msgid "Log in to follow"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:4
 | 
			
		||||
msgctxt "remote follow mfa header"
 | 
			
		||||
msgid "Two-factor authentication"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex:4
 | 
			
		||||
msgctxt "remote follow success"
 | 
			
		||||
msgid "Account followed!"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:7
 | 
			
		||||
msgctxt "placeholder text for account id"
 | 
			
		||||
msgid "Your account ID, e.g. lain@quitter.se"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:8
 | 
			
		||||
msgctxt "remote follow authorization button for following with a remote account"
 | 
			
		||||
msgid "Follow"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:2
 | 
			
		||||
msgctxt "remote follow error"
 | 
			
		||||
msgid "Error: %{error}"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:4
 | 
			
		||||
msgctxt "remote follow header"
 | 
			
		||||
msgid "Remotely follow %{nickname}"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:12
 | 
			
		||||
msgctxt "password reset button"
 | 
			
		||||
msgid "Reset"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex:4
 | 
			
		||||
msgctxt "password reset failed homepage link"
 | 
			
		||||
msgid "Homepage"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex:1
 | 
			
		||||
msgctxt "password reset failed message"
 | 
			
		||||
msgid "Password reset failed"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:8
 | 
			
		||||
msgctxt "password reset form confirm password prompt"
 | 
			
		||||
msgid "Confirmation"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:4
 | 
			
		||||
msgctxt "password reset form password prompt"
 | 
			
		||||
msgid "Password"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/password/invalid_token.html.eex:1
 | 
			
		||||
msgctxt "password reset invalid token message"
 | 
			
		||||
msgid "Invalid Token"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex:2
 | 
			
		||||
msgctxt "password reset successful homepage link"
 | 
			
		||||
msgid "Homepage"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex:1
 | 
			
		||||
msgctxt "password reset successful message"
 | 
			
		||||
msgid "Password changed!"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/feed/feed/tag.atom.eex:15
 | 
			
		||||
#: lib/pleroma/web/templates/feed/feed/tag.rss.eex:7
 | 
			
		||||
msgctxt "tag feed description"
 | 
			
		||||
msgid "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex:1
 | 
			
		||||
msgctxt "oauth authorization exists page title"
 | 
			
		||||
msgid "Authorization exists"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:32
 | 
			
		||||
msgctxt "oauth authorize approve button"
 | 
			
		||||
msgid "Approve"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:30
 | 
			
		||||
msgctxt "oauth authorize cancel button"
 | 
			
		||||
msgid "Cancel"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:23
 | 
			
		||||
msgctxt "oauth authorize message"
 | 
			
		||||
msgid "Application <strong>%{client_name}</strong> is requesting access to your account."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex:1
 | 
			
		||||
msgctxt "oauth authorized page title"
 | 
			
		||||
msgid "Successfully authorized"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex:1
 | 
			
		||||
msgctxt "oauth external provider page title"
 | 
			
		||||
msgid "Sign in with external provider"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex:13
 | 
			
		||||
msgctxt "oauth external provider sign in button"
 | 
			
		||||
msgid "Sign in with %{strategy}"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:54
 | 
			
		||||
msgctxt "oauth login button"
 | 
			
		||||
msgid "Log In"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:51
 | 
			
		||||
msgctxt "oauth login password prompt"
 | 
			
		||||
msgid "Password"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:47
 | 
			
		||||
msgctxt "oauth login username prompt"
 | 
			
		||||
msgid "Username"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:39
 | 
			
		||||
msgctxt "oauth register nickname prompt"
 | 
			
		||||
msgid "Pleroma Handle"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:37
 | 
			
		||||
msgctxt "oauth register nickname unchangeable warning"
 | 
			
		||||
msgid "Choose carefully! You won't be able to change this later. You will be able to change your display name, though."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:18
 | 
			
		||||
msgctxt "oauth register page email prompt"
 | 
			
		||||
msgid "Email"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:10
 | 
			
		||||
msgctxt "oauth register page fill form prompt"
 | 
			
		||||
msgid "If you'd like to register a new account, please provide the details below."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:35
 | 
			
		||||
msgctxt "oauth register page login button"
 | 
			
		||||
msgid "Proceed as existing user"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:31
 | 
			
		||||
msgctxt "oauth register page login password prompt"
 | 
			
		||||
msgid "Password"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:24
 | 
			
		||||
msgctxt "oauth register page login prompt"
 | 
			
		||||
msgid "Alternatively, sign in to connect to existing account."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:27
 | 
			
		||||
msgctxt "oauth register page login username prompt"
 | 
			
		||||
msgid "Name or email"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:14
 | 
			
		||||
msgctxt "oauth register page nickname prompt"
 | 
			
		||||
msgid "Nickname"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:22
 | 
			
		||||
msgctxt "oauth register page register button"
 | 
			
		||||
msgid "Proceed as new user"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:8
 | 
			
		||||
msgctxt "oauth register page title"
 | 
			
		||||
msgid "Registration Details"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:36
 | 
			
		||||
msgctxt "oauth register page title"
 | 
			
		||||
msgid "This is the first time you visit! Please enter your Pleroma handle."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex:2
 | 
			
		||||
msgctxt "oauth scopes message"
 | 
			
		||||
msgid "The following permissions will be granted"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex:2
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex:2
 | 
			
		||||
msgctxt "oauth token code message"
 | 
			
		||||
msgid "Token code is <br>%{token}"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:12
 | 
			
		||||
msgctxt "mfa auth code prompt"
 | 
			
		||||
msgid "Authentication code"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:8
 | 
			
		||||
msgctxt "mfa auth page title"
 | 
			
		||||
msgid "Two-factor authentication"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:23
 | 
			
		||||
msgctxt "mfa auth page use recovery code link"
 | 
			
		||||
msgid "Enter a two-factor recovery code"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:20
 | 
			
		||||
msgctxt "mfa auth verify code button"
 | 
			
		||||
msgid "Verify"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:8
 | 
			
		||||
msgctxt "mfa recover page title"
 | 
			
		||||
msgid "Two-factor recovery"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:12
 | 
			
		||||
msgctxt "mfa recover recovery code prompt"
 | 
			
		||||
msgid "Recovery code"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:23
 | 
			
		||||
msgctxt "mfa recover use 2fa code link"
 | 
			
		||||
msgid "Enter a two-factor code"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:20
 | 
			
		||||
msgctxt "mfa recover verify recovery code button"
 | 
			
		||||
msgid "Verify"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex:8
 | 
			
		||||
msgctxt "static fe profile page remote follow button"
 | 
			
		||||
msgid "Remote follow"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/email/digest.html.eex:163
 | 
			
		||||
msgctxt "digest email header line"
 | 
			
		||||
msgid "Hey %{nickname}, here is what you've missed!"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/email/digest.html.eex:544
 | 
			
		||||
msgctxt "digest email receiver address"
 | 
			
		||||
msgid "The email address you are subscribed as is <a href='mailto:%{@user.email}' style='color: %{color};text-decoration: none;'>%{email}</a>. "
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/email/digest.html.eex:538
 | 
			
		||||
msgctxt "digest email sending reason"
 | 
			
		||||
msgid "You have received this email because you have signed up to receive digest emails from <b>%{instance}</b> Pleroma instance."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/email/digest.html.eex:547
 | 
			
		||||
msgctxt "digest email unsubscribe action"
 | 
			
		||||
msgid "To unsubscribe, please go %{here}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/email/digest.html.eex:547
 | 
			
		||||
msgctxt "digest email unsubscribe action link text"
 | 
			
		||||
msgid "here"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/mailer/subscription/unsubscribe_failure.html.eex:1
 | 
			
		||||
msgctxt "mailer unsubscribe failed message"
 | 
			
		||||
msgid "UNSUBSCRIBE FAILURE"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/mailer/subscription/unsubscribe_success.html.eex:1
 | 
			
		||||
msgctxt "mailer unsubscribe successful message"
 | 
			
		||||
msgid "UNSUBSCRIBE SUCCESSFUL"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/web/templates/email/digest.html.eex:385
 | 
			
		||||
msgctxt "new followers count header"
 | 
			
		||||
msgid "%{count} New Follower"
 | 
			
		||||
msgid_plural "%{count} New Followers"
 | 
			
		||||
msgstr[0] ""
 | 
			
		||||
msgstr[1] ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/emails/user_email.ex:356
 | 
			
		||||
msgctxt "account archive email body - self-requested"
 | 
			
		||||
msgid "<p>You requested a full backup of your Pleroma account. It's ready for download:</p>\n<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/emails/user_email.ex:384
 | 
			
		||||
msgctxt "account archive email subject"
 | 
			
		||||
msgid "Your account archive is ready"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/emails/user_email.ex:188
 | 
			
		||||
msgctxt "approval pending email body"
 | 
			
		||||
msgid "<h3>Awaiting Approval</h3>\n<p>Your account at %{instance_name} is being reviewed by staff. You will receive another email once your account is approved.</p>\n"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/emails/user_email.ex:202
 | 
			
		||||
msgctxt "approval pending email subject"
 | 
			
		||||
msgid "Your account is awaiting approval"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/emails/user_email.ex:158
 | 
			
		||||
msgctxt "confirmation email body"
 | 
			
		||||
msgid "<h3>Thank you for registering on %{instance_name}</h3>\n<p>Email confirmation is required to activate the account.</p>\n<p>Please click the following link to <a href=\"%{confirmation_url}\">activate your account</a>.</p>\n"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/emails/user_email.ex:174
 | 
			
		||||
msgctxt "confirmation email subject"
 | 
			
		||||
msgid "%{instance_name} account confirmation"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/emails/user_email.ex:310
 | 
			
		||||
msgctxt "digest email subject"
 | 
			
		||||
msgid "Your digest from %{instance_name}"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/emails/user_email.ex:81
 | 
			
		||||
msgctxt "password reset email body"
 | 
			
		||||
msgid "<h3>Reset your password at %{instance_name}</h3>\n<p>Someone has requested password change for your account at %{instance_name}.</p>\n<p>If it was you, visit the following link to proceed: <a href=\"%{password_reset_url}\">reset password</a>.</p>\n<p>If it was someone else, nothing to worry about: your data is secure and your password has not been changed.</p>\n"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/emails/user_email.ex:98
 | 
			
		||||
msgctxt "password reset email subject"
 | 
			
		||||
msgid "Password reset"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/emails/user_email.ex:215
 | 
			
		||||
msgctxt "successful registration email body"
 | 
			
		||||
msgid "<h3>Hello @%{nickname},</h3>\n<p>Your account at %{instance_name} has been registered successfully.</p>\n<p>No further action is required to activate your account.</p>\n"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/emails/user_email.ex:231
 | 
			
		||||
msgctxt "successful registration email subject"
 | 
			
		||||
msgid "Account registered on %{instance_name}"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/emails/user_email.ex:119
 | 
			
		||||
msgctxt "user invitation email body"
 | 
			
		||||
msgid "<h3>You are invited to %{instance_name}</h3>\n<p>%{inviter_name} invites you to join %{instance_name}, an instance of Pleroma federated social networking platform.</p>\n<p>Click the following link to register: <a href=\"%{registration_url}\">accept invitation</a>.</p>\n"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/emails/user_email.ex:136
 | 
			
		||||
msgctxt "user invitation email subject"
 | 
			
		||||
msgid "Invitation to %{instance_name}"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/emails/user_email.ex:53
 | 
			
		||||
msgctxt "welcome email html body"
 | 
			
		||||
msgid "Welcome to %{instance_name}!"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/emails/user_email.ex:41
 | 
			
		||||
msgctxt "welcome email subject"
 | 
			
		||||
msgid "Welcome to %{instance_name}!"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/emails/user_email.ex:65
 | 
			
		||||
msgctxt "welcome email text body"
 | 
			
		||||
msgid "Welcome to %{instance_name}!"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#, elixir-format
 | 
			
		||||
#: lib/pleroma/emails/user_email.ex:368
 | 
			
		||||
msgctxt "account archive email body - admin requested"
 | 
			
		||||
msgid "<p>Admin @%{admin_nickname} requested a full backup of your Pleroma account. It's ready for download:</p>\n<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
 | 
			
		||||
msgstr ""
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
defmodule Pleroma.Repo.Migrations.AddLanguageToUsers do
 | 
			
		||||
  use Ecto.Migration
 | 
			
		||||
 | 
			
		||||
  def change do
 | 
			
		||||
    alter table(:users) do
 | 
			
		||||
      add_if_not_exists(:language, :string)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										31
									
								
								test/fixtures/owncast-note-with-attachment.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								test/fixtures/owncast-note-with-attachment.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,31 @@
 | 
			
		|||
{
 | 
			
		||||
  "attachment": {
 | 
			
		||||
    "content": "Live stream preview",
 | 
			
		||||
    "type": "Image",
 | 
			
		||||
    "url": "https://owncast.localhost.localdomain/preview.gif?us=KjfNX387gm"
 | 
			
		||||
  },
 | 
			
		||||
  "attributedTo": "https://owncast.localhost.localdomain/federation/user/streamer",
 | 
			
		||||
  "audience": "https://www.w3.org/ns/activitystreams#Public",
 | 
			
		||||
  "content": "<p>I've gone live!</p><p></p><p><a class=\"hashtag\" href=\"https://directory.owncast.online/tags/owncast\">#owncast</a> <a class=\"hashtag\" href=\"https://directory.owncast.online/tags/streaming\">#streaming</a></p><a href=\"https://owncast.localhost.localdomain\">https://owncast.localhost.localdomain</a>",
 | 
			
		||||
  "id": "https://owncast.localhost.localdomain/federation/KjBNuq8ng",
 | 
			
		||||
  "published": "2022-04-17T15:42:03Z",
 | 
			
		||||
  "tag": [
 | 
			
		||||
    {
 | 
			
		||||
      "href": "https://directory.owncast.online/tags/owncast",
 | 
			
		||||
      "name": "#owncast",
 | 
			
		||||
      "type": "Hashtag"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "href": "https://directory.owncast.online/tags/streaming",
 | 
			
		||||
      "name": "#streaming",
 | 
			
		||||
      "type": "Hashtag"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "href": "https://directory.owncast.online/tags/owncast",
 | 
			
		||||
      "name": "#owncast",
 | 
			
		||||
      "type": "Hashtag"
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "to": "https://www.w3.org/ns/activitystreams#Public",
 | 
			
		||||
  "type": "Note"
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -53,7 +53,13 @@ test "Sends digest to the given user" do
 | 
			
		|||
 | 
			
		||||
      assert_email_sent(
 | 
			
		||||
        to: {user2.name, user2.email},
 | 
			
		||||
        html_body: ~r/here is what you've missed!/i
 | 
			
		||||
        html_body:
 | 
			
		||||
          Regex.compile!(
 | 
			
		||||
            "here is what you've missed!"
 | 
			
		||||
            |> Phoenix.HTML.html_escape()
 | 
			
		||||
            |> Phoenix.HTML.safe_to_string(),
 | 
			
		||||
            "i"
 | 
			
		||||
          )
 | 
			
		||||
      )
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -56,4 +56,16 @@ test "build approval pending email" do
 | 
			
		|||
    assert email.subject == "Your account is awaiting approval"
 | 
			
		||||
    assert email.html_body =~ "Awaiting Approval"
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  test "email i18n" do
 | 
			
		||||
    user = insert(:user, language: "en_test")
 | 
			
		||||
    email = UserEmail.approval_pending_email(user)
 | 
			
		||||
    assert email.subject == "xxYour account is awaiting approvalxx"
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  test "email i18n should fallback to default locale if user language is unsupported" do
 | 
			
		||||
    user = insert(:user, language: "unsupported")
 | 
			
		||||
    email = UserEmail.approval_pending_email(user)
 | 
			
		||||
    assert email.subject == "Your account is awaiting approval"
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
Some files were not shown because too many files have changed in this diff Show more
		Loading…
	
		Reference in a new issue