Merge upstream (#81)
This commit is contained in:
@@ -47,6 +47,7 @@ class Account < ApplicationRecord
|
||||
include AccountInteractions
|
||||
include Attachmentable
|
||||
include Remotable
|
||||
include EmojiHelper
|
||||
|
||||
# Local users
|
||||
has_one :user, inverse_of: :account
|
||||
@@ -129,7 +130,7 @@ class Account < ApplicationRecord
|
||||
end
|
||||
|
||||
def subscription(webhook_url)
|
||||
OStatus2::Subscription.new(remote_url, secret: secret, lease_seconds: 86_400 * 30, webhook: webhook_url, hub: hub_url)
|
||||
OStatus2::Subscription.new(remote_url, secret: secret, lease_seconds: 30.days.seconds, webhook: webhook_url, hub: hub_url)
|
||||
end
|
||||
|
||||
def save_with_optional_media!
|
||||
@@ -240,9 +241,18 @@ class Account < ApplicationRecord
|
||||
|
||||
before_create :generate_keys
|
||||
before_validation :normalize_domain
|
||||
before_validation :prepare_contents, if: :local?
|
||||
|
||||
private
|
||||
|
||||
def prepare_contents
|
||||
display_name&.strip!
|
||||
note&.strip!
|
||||
|
||||
self.display_name = emojify(display_name)
|
||||
self.note = emojify(note)
|
||||
end
|
||||
|
||||
def generate_keys
|
||||
return unless local?
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Remotable
|
||||
include HttpHelper
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
@@ -20,7 +19,7 @@ module Remotable
|
||||
return if !%w(http https).include?(parsed_url.scheme) || parsed_url.host.empty? || self[attribute_name] == url
|
||||
|
||||
begin
|
||||
response = http_client.get(url)
|
||||
response = Request.new(:get, url).perform
|
||||
|
||||
return if response.code != 200
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# severity :integer default("silence")
|
||||
# reject_media :boolean
|
||||
# reject_media :boolean default(FALSE), not null
|
||||
#
|
||||
|
||||
class DomainBlock < ApplicationRecord
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# id :integer not null, primary key
|
||||
# account_id :integer not null
|
||||
# type :integer not null
|
||||
# approved :boolean
|
||||
# approved :boolean default(FALSE), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# data_file_name :string
|
||||
|
||||
@@ -3,6 +3,17 @@
|
||||
#
|
||||
# Table name: session_activations
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# user_id :integer not null
|
||||
# session_id :string not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# user_agent :string default(""), not null
|
||||
# ip :inet
|
||||
# access_token_id :integer
|
||||
# web_push_subscription_id :integer
|
||||
#
|
||||
|
||||
# id :integer not null, primary key
|
||||
# user_id :integer not null
|
||||
# session_id :string not null
|
||||
@@ -15,6 +26,7 @@
|
||||
|
||||
class SessionActivation < ApplicationRecord
|
||||
belongs_to :access_token, class_name: 'Doorkeeper::AccessToken', dependent: :destroy
|
||||
belongs_to :web_push_subscription, class_name: 'Web::PushSubscription', dependent: :destroy
|
||||
|
||||
delegate :token,
|
||||
to: :access_token,
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
# in_reply_to_id :integer
|
||||
# reblog_of_id :integer
|
||||
# url :string
|
||||
# sensitive :boolean default(FALSE)
|
||||
# sensitive :boolean default(FALSE), not null
|
||||
# visibility :integer default("public"), not null
|
||||
# in_reply_to_account_id :integer
|
||||
# application_id :integer
|
||||
# spoiler_text :text default(""), not null
|
||||
# reply :boolean default(FALSE)
|
||||
# reply :boolean default(FALSE), not null
|
||||
# favourites_count :integer default(0), not null
|
||||
# reblogs_count :integer default(0), not null
|
||||
# language :string
|
||||
@@ -29,6 +29,7 @@ class Status < ApplicationRecord
|
||||
include Streamable
|
||||
include Cacheable
|
||||
include StatusThreadingConcern
|
||||
include EmojiHelper
|
||||
|
||||
enum visibility: [:public, :unlisted, :private, :direct], _suffix: :visibility
|
||||
|
||||
@@ -120,10 +121,11 @@ class Status < ApplicationRecord
|
||||
!sensitive? && media_attachments.any?
|
||||
end
|
||||
|
||||
before_validation :prepare_contents
|
||||
before_validation :prepare_contents, if: :local?
|
||||
before_validation :set_reblog
|
||||
before_validation :set_visibility
|
||||
before_validation :set_conversation
|
||||
before_validation :set_sensitivity
|
||||
|
||||
class << self
|
||||
def not_in_filtered_languages(account)
|
||||
@@ -240,6 +242,9 @@ class Status < ApplicationRecord
|
||||
def prepare_contents
|
||||
text&.strip!
|
||||
spoiler_text&.strip!
|
||||
|
||||
self.text = emojify(text)
|
||||
self.spoiler_text = emojify(spoiler_text)
|
||||
end
|
||||
|
||||
def set_reblog
|
||||
@@ -248,6 +253,11 @@ class Status < ApplicationRecord
|
||||
|
||||
def set_visibility
|
||||
self.visibility = (account.locked? ? :private : :public) if visibility.nil?
|
||||
self.sensitive = false if sensitive.nil?
|
||||
end
|
||||
|
||||
def set_sensitivity
|
||||
self.sensitive = sensitive || spoiler_text.present?
|
||||
end
|
||||
|
||||
def set_conversation
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: subscriptions
|
||||
@@ -13,11 +12,12 @@
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# last_successful_delivery_at :datetime
|
||||
# domain :string
|
||||
#
|
||||
|
||||
class Subscription < ApplicationRecord
|
||||
MIN_EXPIRATION = 7.days.seconds.to_i
|
||||
MAX_EXPIRATION = 30.days.seconds.to_i
|
||||
MIN_EXPIRATION = 1.day.to_i
|
||||
MAX_EXPIRATION = 30.days.to_i
|
||||
|
||||
belongs_to :account, required: true
|
||||
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
class Tag < ApplicationRecord
|
||||
has_and_belongs_to_many :statuses
|
||||
|
||||
HASHTAG_RE = /(?:^|[^\/\)\w])#([[:word:]_]*[[:alpha:]_][[:word:]_]*)/i
|
||||
HASHTAG_NAME_RE = '[[:word:]_]*[[:alpha:]_][[:word:]_]*'
|
||||
HASHTAG_RE = /(?:^|[^\/\)\w])#(#{HASHTAG_NAME_RE})/i
|
||||
|
||||
validates :name, presence: true, uniqueness: true
|
||||
validates :name, presence: true, uniqueness: true, format: { with: /\A#{HASHTAG_NAME_RE}\z/i }
|
||||
|
||||
def to_param
|
||||
name
|
||||
@@ -23,7 +24,7 @@ class Tag < ApplicationRecord
|
||||
class << self
|
||||
def search_for(term, limit = 5)
|
||||
pattern = sanitize_sql_like(term) + '%'
|
||||
Tag.where('name like ?', pattern).order(:name).limit(limit)
|
||||
Tag.where('lower(name) like lower(?)', pattern).order(:name).limit(limit)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
# last_sign_in_at :datetime
|
||||
# current_sign_in_ip :inet
|
||||
# last_sign_in_ip :inet
|
||||
# admin :boolean default(FALSE)
|
||||
# admin :boolean default(FALSE), not null
|
||||
# confirmation_token :string
|
||||
# confirmed_at :datetime
|
||||
# confirmation_sent_at :datetime
|
||||
@@ -27,7 +27,7 @@
|
||||
# encrypted_otp_secret_iv :string
|
||||
# encrypted_otp_secret_salt :string
|
||||
# consumed_timestep :integer
|
||||
# otp_required_for_login :boolean
|
||||
# otp_required_for_login :boolean default(FALSE), not null
|
||||
# last_emailed_at :datetime
|
||||
# otp_backup_codes :string is an Array
|
||||
# filtered_languages :string default([]), not null, is an Array
|
||||
@@ -99,6 +99,10 @@ class User < ApplicationRecord
|
||||
settings.system_font_ui
|
||||
end
|
||||
|
||||
def setting_noindex
|
||||
settings.noindex
|
||||
end
|
||||
|
||||
def activate_session(request)
|
||||
session_activations.activate(session_id: SecureRandom.hex,
|
||||
user_agent: request.user_agent,
|
||||
@@ -113,6 +117,10 @@ class User < ApplicationRecord
|
||||
session_activations.active? id
|
||||
end
|
||||
|
||||
def web_push_subscription(session)
|
||||
session.web_push_subscription.nil? ? nil : session.web_push_subscription.as_payload
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def send_devise_notification(notification, *args)
|
||||
|
||||
190
app/models/web/push_subscription.rb
Normal file
190
app/models/web/push_subscription.rb
Normal file
@@ -0,0 +1,190 @@
|
||||
# frozen_string_literal: true
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: web_push_subscriptions
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# endpoint :string not null
|
||||
# key_p256dh :string not null
|
||||
# key_auth :string not null
|
||||
# data :json
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class Web::PushSubscription < ApplicationRecord
|
||||
include RoutingHelper
|
||||
include StreamEntriesHelper
|
||||
include ActionView::Helpers::TranslationHelper
|
||||
include ActionView::Helpers::SanitizeHelper
|
||||
|
||||
has_one :session_activation
|
||||
|
||||
before_create :send_welcome_notification
|
||||
|
||||
def push(notification)
|
||||
return unless pushable? notification
|
||||
|
||||
name = display_name notification.from_account
|
||||
title = title_str(name, notification)
|
||||
body = body_str notification
|
||||
dir = dir_str body
|
||||
url = url_str notification
|
||||
image = image_str notification
|
||||
actions = actions_arr notification
|
||||
|
||||
access_token = actions.empty? ? nil : find_or_create_access_token(notification).token
|
||||
nsfw = notification.target_status.nil? || notification.target_status.spoiler_text.empty? ? nil : notification.target_status.spoiler_text
|
||||
|
||||
# TODO: Make sure that the payload does not exceed 4KB - Webpush::PayloadTooLarge
|
||||
# TODO: Queue the requests - Webpush::TooManyRequests
|
||||
Webpush.payload_send(
|
||||
message: JSON.generate(
|
||||
title: title,
|
||||
dir: dir,
|
||||
image: image,
|
||||
badge: full_asset_url('badge.png'),
|
||||
tag: notification.id,
|
||||
timestamp: notification.created_at,
|
||||
icon: notification.from_account.avatar_static_url,
|
||||
data: {
|
||||
content: decoder.decode(strip_tags(body)),
|
||||
nsfw: nsfw.nil? ? nil : decoder.decode(strip_tags(nsfw)),
|
||||
url: url,
|
||||
actions: actions,
|
||||
access_token: access_token,
|
||||
}
|
||||
),
|
||||
endpoint: endpoint,
|
||||
p256dh: key_p256dh,
|
||||
auth: key_auth,
|
||||
vapid: {
|
||||
# subject: "mailto:#{Setting.site_contact_email}",
|
||||
private_key: Rails.configuration.x.vapid_private_key,
|
||||
public_key: Rails.configuration.x.vapid_public_key,
|
||||
},
|
||||
ttl: 40 * 60 * 60 # 48 hours
|
||||
)
|
||||
end
|
||||
|
||||
def as_payload
|
||||
payload = {
|
||||
id: id,
|
||||
endpoint: endpoint,
|
||||
}
|
||||
|
||||
payload[:alerts] = data['alerts'] if data && data.key?('alerts')
|
||||
|
||||
payload
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def title_str(name, notification)
|
||||
case notification.type
|
||||
when :mention then translate('push_notifications.mention.title', name: name)
|
||||
when :follow then translate('push_notifications.follow.title', name: name)
|
||||
when :favourite then translate('push_notifications.favourite.title', name: name)
|
||||
when :reblog then translate('push_notifications.reblog.title', name: name)
|
||||
end
|
||||
end
|
||||
|
||||
def body_str(notification)
|
||||
case notification.type
|
||||
when :mention then notification.target_status.text
|
||||
when :follow then notification.from_account.note
|
||||
when :favourite then notification.target_status.text
|
||||
when :reblog then notification.target_status.text
|
||||
end
|
||||
end
|
||||
|
||||
def url_str(notification)
|
||||
case notification.type
|
||||
when :mention then web_url("statuses/#{notification.target_status.id}")
|
||||
when :follow then web_url("accounts/#{notification.from_account.id}")
|
||||
when :favourite then web_url("statuses/#{notification.target_status.id}")
|
||||
when :reblog then web_url("statuses/#{notification.target_status.id}")
|
||||
end
|
||||
end
|
||||
|
||||
def actions_arr(notification)
|
||||
actions =
|
||||
case notification.type
|
||||
when :mention then [
|
||||
{
|
||||
title: translate('push_notifications.mention.action_favourite'),
|
||||
icon: full_asset_url('emoji/2764.png'),
|
||||
todo: 'request',
|
||||
method: 'POST',
|
||||
action: "/api/v1/statuses/#{notification.target_status.id}/favourite",
|
||||
},
|
||||
]
|
||||
else []
|
||||
end
|
||||
|
||||
should_hide = notification.type.equal?(:mention) && !notification.target_status.nil? && (notification.target_status.sensitive || !notification.target_status.spoiler_text.empty?)
|
||||
can_boost = notification.type.equal?(:mention) && !notification.target_status.nil? && !notification.target_status.hidden?
|
||||
|
||||
if should_hide
|
||||
actions.insert(0, title: translate('push_notifications.mention.action_expand'), icon: full_asset_url('emoji/1f441.png'), todo: 'expand', action: 'expand')
|
||||
end
|
||||
|
||||
if can_boost
|
||||
actions << { title: translate('push_notifications.mention.action_boost'), icon: full_asset_url('emoji/1f504.png'), todo: 'request', method: 'POST', action: "/api/v1/statuses/#{notification.target_status.id}/reblog" }
|
||||
end
|
||||
|
||||
actions
|
||||
end
|
||||
|
||||
def image_str(notification)
|
||||
return nil if notification.target_status.nil? || notification.target_status.media_attachments.empty?
|
||||
|
||||
full_asset_url(notification.target_status.media_attachments.first.file.url(:small))
|
||||
end
|
||||
|
||||
def dir_str(body)
|
||||
rtl?(body) ? 'rtl' : 'ltr'
|
||||
end
|
||||
|
||||
def pushable?(notification)
|
||||
data && data.key?('alerts') && data['alerts'][notification.type.to_s]
|
||||
end
|
||||
|
||||
def send_welcome_notification
|
||||
Webpush.payload_send(
|
||||
message: JSON.generate(
|
||||
title: translate('push_notifications.subscribed.title'),
|
||||
icon: full_asset_url('android-chrome-192x192.png'),
|
||||
badge: full_asset_url('badge.png'),
|
||||
data: {
|
||||
content: translate('push_notifications.subscribed.body'),
|
||||
actions: [],
|
||||
url: web_url('notifications'),
|
||||
}
|
||||
),
|
||||
endpoint: endpoint,
|
||||
p256dh: key_p256dh,
|
||||
auth: key_auth,
|
||||
vapid: {
|
||||
# subject: "mailto:#{Setting.site_contact_email}",
|
||||
private_key: Rails.configuration.x.vapid_private_key,
|
||||
public_key: Rails.configuration.x.vapid_public_key,
|
||||
},
|
||||
ttl: 5 * 60 # 5 minutes
|
||||
)
|
||||
end
|
||||
|
||||
def find_or_create_access_token(notification)
|
||||
Doorkeeper::AccessToken.find_or_create_for(
|
||||
Doorkeeper::Application.find_by(superapp: true),
|
||||
notification.account.user.id,
|
||||
Doorkeeper::OAuth::Scopes.from_string('read write follow'),
|
||||
Doorkeeper.configuration.access_token_expires_in,
|
||||
Doorkeeper.configuration.refresh_token_enabled?
|
||||
)
|
||||
end
|
||||
|
||||
def decoder
|
||||
@decoder ||= HTMLEntities.new
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user