mirror of
https://github.com/iv-org/invidious.git
synced 2024-12-20 13:14:27 -05:00
Refactored code and added badges to Search but many dummies because of the way components/item works
This commit is contained in:
parent
a2578ac6b4
commit
00df3e2c40
@ -42,7 +42,7 @@ def get_about_info(ucid, locale) : AboutChannel
|
|||||||
if !initdata.has_key?("metadata")
|
if !initdata.has_key?("metadata")
|
||||||
auto_generated = true
|
auto_generated = true
|
||||||
end
|
end
|
||||||
verified = false
|
|
||||||
if auto_generated
|
if auto_generated
|
||||||
author = initdata["header"]["interactiveTabbedHeaderRenderer"]["title"]["simpleText"].as_s
|
author = initdata["header"]["interactiveTabbedHeaderRenderer"]["title"]["simpleText"].as_s
|
||||||
author_url = initdata["microformat"]["microformatDataRenderer"]["urlCanonical"].as_s
|
author_url = initdata["microformat"]["microformatDataRenderer"]["urlCanonical"].as_s
|
||||||
@ -71,10 +71,9 @@ def get_about_info(ucid, locale) : AboutChannel
|
|||||||
# if banner.includes? "channels/c4/default_banner"
|
# if banner.includes? "channels/c4/default_banner"
|
||||||
# banner = nil
|
# banner = nil
|
||||||
# end
|
# end
|
||||||
badges = initdata["header"]["c4TabbedHeaderRenderer"]?.try &.["badges"]?
|
author_verified_badges = initdata["header"]?.try &.["c4TabbedHeaderRenderer"]?.try &.["badges"]?
|
||||||
if !badges.nil?
|
|
||||||
verified = true
|
author_verified = (author_verified_badges && author_verified_badges.size > 0)
|
||||||
end
|
|
||||||
description = initdata["metadata"]["channelMetadataRenderer"]?.try &.["description"]?.try &.as_s? || ""
|
description = initdata["metadata"]["channelMetadataRenderer"]?.try &.["description"]?.try &.as_s? || ""
|
||||||
description_html = HTML.escape(description)
|
description_html = HTML.escape(description)
|
||||||
|
|
||||||
@ -132,7 +131,7 @@ def get_about_info(ucid, locale) : AboutChannel
|
|||||||
is_family_friendly: is_family_friendly,
|
is_family_friendly: is_family_friendly,
|
||||||
allowed_regions: allowed_regions,
|
allowed_regions: allowed_regions,
|
||||||
tabs: tabs,
|
tabs: tabs,
|
||||||
verified: verified,
|
verified: author_verified || false,
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ struct ChannelVideo
|
|||||||
property live_now : Bool = false
|
property live_now : Bool = false
|
||||||
property premiere_timestamp : Time? = nil
|
property premiere_timestamp : Time? = nil
|
||||||
property views : Int64? = nil
|
property views : Int64? = nil
|
||||||
|
property author_verified : Bool #TODO currently a dummy
|
||||||
|
|
||||||
def to_json(locale, json : JSON::Builder)
|
def to_json(locale, json : JSON::Builder)
|
||||||
json.object do
|
json.object do
|
||||||
@ -218,6 +219,7 @@ def fetch_channel(ucid, pull_all_videos : Bool)
|
|||||||
live_now: live_now,
|
live_now: live_now,
|
||||||
premiere_timestamp: premiere_timestamp,
|
premiere_timestamp: premiere_timestamp,
|
||||||
views: views,
|
views: views,
|
||||||
|
author_verified: false, #TODO dummy for components/item.ecr
|
||||||
})
|
})
|
||||||
|
|
||||||
LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Updating or inserting video")
|
LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Updating or inserting video")
|
||||||
@ -255,6 +257,7 @@ def fetch_channel(ucid, pull_all_videos : Bool)
|
|||||||
live_now: video.live_now,
|
live_now: video.live_now,
|
||||||
premiere_timestamp: video.premiere_timestamp,
|
premiere_timestamp: video.premiere_timestamp,
|
||||||
views: video.views,
|
views: video.views,
|
||||||
|
author_verified: false, #TODO dummy for components/item.ecr
|
||||||
}) }
|
}) }
|
||||||
|
|
||||||
videos.each do |video|
|
videos.each do |video|
|
||||||
|
@ -144,8 +144,8 @@ def fetch_youtube_comments(id, cursor, format, locale, thin_mode, region, sort_b
|
|||||||
|
|
||||||
content_html = node_comment["contentText"]?.try { |t| parse_content(t) } || ""
|
content_html = node_comment["contentText"]?.try { |t| parse_content(t) } || ""
|
||||||
author = node_comment["authorText"]?.try &.["simpleText"]? || ""
|
author = node_comment["authorText"]?.try &.["simpleText"]? || ""
|
||||||
verified = node_comment["authorCommentBadge"]? != nil
|
verified = (node_comment["authorCommentBadge"]? != nil)
|
||||||
json.field "verified", verified
|
json.field "verified", (verified || false)
|
||||||
json.field "author", author
|
json.field "author", author
|
||||||
json.field "authorThumbnails" do
|
json.field "authorThumbnails" do
|
||||||
json.array do
|
json.array do
|
||||||
@ -329,7 +329,7 @@ def template_youtube_comments(comments, locale, thin_mode, is_replies = false)
|
|||||||
end
|
end
|
||||||
|
|
||||||
author_name = HTML.escape(child["author"].as_s)
|
author_name = HTML.escape(child["author"].as_s)
|
||||||
if child["verified"].as_bool
|
if child["verified"]?.try &.as_bool
|
||||||
author_name += "<i class=\"icon ion ion-md-checkmark-circle\"></i>"
|
author_name += "<i class=\"icon ion ion-md-checkmark-circle\"></i>"
|
||||||
end
|
end
|
||||||
html << <<-END_HTML
|
html << <<-END_HTML
|
||||||
|
@ -12,6 +12,7 @@ struct SearchVideo
|
|||||||
property live_now : Bool
|
property live_now : Bool
|
||||||
property premium : Bool
|
property premium : Bool
|
||||||
property premiere_timestamp : Time?
|
property premiere_timestamp : Time?
|
||||||
|
property author_verified : Bool
|
||||||
|
|
||||||
def to_xml(auto_generated, query_params, xml : XML::Builder)
|
def to_xml(auto_generated, query_params, xml : XML::Builder)
|
||||||
query_params["v"] = self.id
|
query_params["v"] = self.id
|
||||||
@ -129,6 +130,7 @@ struct SearchPlaylist
|
|||||||
property video_count : Int32
|
property video_count : Int32
|
||||||
property videos : Array(SearchPlaylistVideo)
|
property videos : Array(SearchPlaylistVideo)
|
||||||
property thumbnail : String?
|
property thumbnail : String?
|
||||||
|
property author_verified : Bool
|
||||||
|
|
||||||
def to_json(locale : String?, json : JSON::Builder)
|
def to_json(locale : String?, json : JSON::Builder)
|
||||||
json.object do
|
json.object do
|
||||||
@ -140,7 +142,7 @@ struct SearchPlaylist
|
|||||||
json.field "author", self.author
|
json.field "author", self.author
|
||||||
json.field "authorId", self.ucid
|
json.field "authorId", self.ucid
|
||||||
json.field "authorUrl", "/channel/#{self.ucid}"
|
json.field "authorUrl", "/channel/#{self.ucid}"
|
||||||
|
json.field "authorVerified", self.author_verified
|
||||||
json.field "videoCount", self.video_count
|
json.field "videoCount", self.video_count
|
||||||
json.field "videos" do
|
json.field "videos" do
|
||||||
json.array do
|
json.array do
|
||||||
@ -182,6 +184,7 @@ struct SearchChannel
|
|||||||
property video_count : Int32
|
property video_count : Int32
|
||||||
property description_html : String
|
property description_html : String
|
||||||
property auto_generated : Bool
|
property auto_generated : Bool
|
||||||
|
property author_verified : Bool
|
||||||
|
|
||||||
def to_json(locale : String?, json : JSON::Builder)
|
def to_json(locale : String?, json : JSON::Builder)
|
||||||
json.object do
|
json.object do
|
||||||
@ -189,7 +192,7 @@ struct SearchChannel
|
|||||||
json.field "author", self.author
|
json.field "author", self.author
|
||||||
json.field "authorId", self.ucid
|
json.field "authorId", self.ucid
|
||||||
json.field "authorUrl", "/channel/#{self.ucid}"
|
json.field "authorUrl", "/channel/#{self.ucid}"
|
||||||
|
json.field "authorVerified", self.author_verified
|
||||||
json.field "authorThumbnails" do
|
json.field "authorThumbnails" do
|
||||||
json.array do
|
json.array do
|
||||||
qualities = {32, 48, 76, 100, 176, 512}
|
qualities = {32, 48, 76, 100, 176, 512}
|
||||||
|
@ -8,6 +8,10 @@ struct MixVideo
|
|||||||
property length_seconds : Int32
|
property length_seconds : Int32
|
||||||
property index : Int32
|
property index : Int32
|
||||||
property rdid : String
|
property rdid : String
|
||||||
|
|
||||||
|
def author_verified
|
||||||
|
false #TODO dummy
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
struct Mix
|
struct Mix
|
||||||
|
@ -234,6 +234,10 @@ struct InvidiousPlaylist
|
|||||||
0_i64
|
0_i64
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def author_verified
|
||||||
|
false # TODO dummy for components/item.ecr
|
||||||
|
end
|
||||||
|
|
||||||
def description_html
|
def description_html
|
||||||
HTML.escape(self.description)
|
HTML.escape(self.description)
|
||||||
end
|
end
|
||||||
@ -252,6 +256,7 @@ def create_playlist(title, privacy, user)
|
|||||||
updated: Time.utc,
|
updated: Time.utc,
|
||||||
privacy: privacy,
|
privacy: privacy,
|
||||||
index: [] of Int64,
|
index: [] of Int64,
|
||||||
|
author_verified: false, # TODO dummy for components/item.ecr
|
||||||
})
|
})
|
||||||
|
|
||||||
Invidious::Database::Playlists.insert(playlist)
|
Invidious::Database::Playlists.insert(playlist)
|
||||||
@ -270,6 +275,7 @@ def subscribe_playlist(user, playlist)
|
|||||||
updated: playlist.updated,
|
updated: playlist.updated,
|
||||||
privacy: PlaylistPrivacy::Private,
|
privacy: PlaylistPrivacy::Private,
|
||||||
index: [] of Int64,
|
index: [] of Int64,
|
||||||
|
author_verified: false, # TODO dummy for components/item.ecr
|
||||||
})
|
})
|
||||||
|
|
||||||
Invidious::Database::Playlists.insert(playlist)
|
Invidious::Database::Playlists.insert(playlist)
|
||||||
|
@ -182,6 +182,7 @@ module Invidious::Routes::Feeds
|
|||||||
paid: false,
|
paid: false,
|
||||||
premium: false,
|
premium: false,
|
||||||
premiere_timestamp: nil,
|
premiere_timestamp: nil,
|
||||||
|
author_verified: false, #TODO real value
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -414,6 +415,7 @@ module Invidious::Routes::Feeds
|
|||||||
live_now: video.live_now,
|
live_now: video.live_now,
|
||||||
premiere_timestamp: video.premiere_timestamp,
|
premiere_timestamp: video.premiere_timestamp,
|
||||||
views: video.views,
|
views: video.views,
|
||||||
|
author_verified: false, #TODO dummy for components/item.ecr
|
||||||
})
|
})
|
||||||
|
|
||||||
was_insert = Invidious::Database::ChannelVideos.insert(video, with_premiere_timestamp: true)
|
was_insert = Invidious::Database::ChannelVideos.insert(video, with_premiere_timestamp: true)
|
||||||
|
@ -1047,7 +1047,9 @@ def extract_video_info(video_id : String, proxy_region : String? = nil, context_
|
|||||||
|
|
||||||
author_info = video_secondary_renderer.try &.dig?("owner", "videoOwnerRenderer")
|
author_info = video_secondary_renderer.try &.dig?("owner", "videoOwnerRenderer")
|
||||||
author_thumbnail = author_info.try &.dig?("thumbnail", "thumbnails", 0, "url")
|
author_thumbnail = author_info.try &.dig?("thumbnail", "thumbnails", 0, "url")
|
||||||
params["authorVerified"] = JSON::Any.new(author_info.try &.["badges"]? != nil)
|
author_verified_badge = author_info.try &.["badges"]?
|
||||||
|
|
||||||
|
params["authorVerified"] = JSON::Any.new((author_verified_badge && author_verified_badge.size > 0) || false)
|
||||||
params["authorThumbnail"] = JSON::Any.new(author_thumbnail.try &.as_s || "")
|
params["authorThumbnail"] = JSON::Any.new(author_thumbnail.try &.as_s || "")
|
||||||
|
|
||||||
params["subCountText"] = JSON::Any.new(author_info.try &.["subscriberCountText"]?
|
params["subCountText"] = JSON::Any.new(author_info.try &.["subscriberCountText"]?
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
<img loading="lazy" style="width:56.25%" src="/ggpht<%= URI.parse(item.author_thumbnail).request_target.gsub(/=s\d+/, "=s176") %>"/>
|
<img loading="lazy" style="width:56.25%" src="/ggpht<%= URI.parse(item.author_thumbnail).request_target.gsub(/=s\d+/, "=s176") %>"/>
|
||||||
</center>
|
</center>
|
||||||
<% end %>
|
<% end %>
|
||||||
<p dir="auto"><%= HTML.escape(item.author) %></p>
|
<p dir="auto"><%= HTML.escape(item.author) %><% if !item.author_verified.nil? && item.author_verified %><i class="icon ion ion-md-checkmark-circle"></i><% end %></p>
|
||||||
</a>
|
</a>
|
||||||
<p><%= translate_count(locale, "generic_subscribers_count", item.subscriber_count, NumberFormatting::Separator) %></p>
|
<p><%= translate_count(locale, "generic_subscribers_count", item.subscriber_count, NumberFormatting::Separator) %></p>
|
||||||
<% if !item.auto_generated %><p><%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %></p><% end %>
|
<% if !item.auto_generated %><p><%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %></p><% end %>
|
||||||
@ -30,7 +30,7 @@
|
|||||||
<p dir="auto"><%= HTML.escape(item.title) %></p>
|
<p dir="auto"><%= HTML.escape(item.title) %></p>
|
||||||
</a>
|
</a>
|
||||||
<a href="/channel/<%= item.ucid %>">
|
<a href="/channel/<%= item.ucid %>">
|
||||||
<p dir="auto"><b><%= HTML.escape(item.author) %></b></p>
|
<p dir="auto"><b><%= HTML.escape(item.author) %><% if !item.author_verified.nil? && item.author_verified %><i class="icon ion ion-md-checkmark-circle"></i><% end %></b></p>
|
||||||
</a>
|
</a>
|
||||||
<% when MixVideo %>
|
<% when MixVideo %>
|
||||||
<a href="/watch?v=<%= item.id %>&list=<%= item.rdid %>">
|
<a href="/watch?v=<%= item.id %>&list=<%= item.rdid %>">
|
||||||
@ -45,7 +45,7 @@
|
|||||||
<p dir="auto"><%= HTML.escape(item.title) %></p>
|
<p dir="auto"><%= HTML.escape(item.title) %></p>
|
||||||
</a>
|
</a>
|
||||||
<a href="/channel/<%= item.ucid %>">
|
<a href="/channel/<%= item.ucid %>">
|
||||||
<p dir="auto"><b><%= HTML.escape(item.author) %></b></p>
|
<p dir="auto"><b><%= HTML.escape(item.author) %><% if !item.author_verified.nil? && item.author_verified %><i class="icon ion ion-md-checkmark-circle"></i><% end %></b></p>
|
||||||
</a>
|
</a>
|
||||||
<% when PlaylistVideo %>
|
<% when PlaylistVideo %>
|
||||||
<a style="width:100%" href="/watch?v=<%= item.id %>&list=<%= item.plid %>&index=<%= item.index %>">
|
<a style="width:100%" href="/watch?v=<%= item.id %>&list=<%= item.plid %>&index=<%= item.index %>">
|
||||||
@ -142,7 +142,7 @@
|
|||||||
|
|
||||||
<div class="video-card-row flexible">
|
<div class="video-card-row flexible">
|
||||||
<div class="flex-left"><a href="/channel/<%= item.ucid %>">
|
<div class="flex-left"><a href="/channel/<%= item.ucid %>">
|
||||||
<p class="channel-name" dir="auto"><%= HTML.escape(item.author) %></p>
|
<p class="channel-name" dir="auto"><%= HTML.escape(item.author) %><% if !item.author_verified.nil? && item.author_verified %><i class="icon ion ion-md-checkmark-circle"></i><% end %></p>
|
||||||
</a></div>
|
</a></div>
|
||||||
|
|
||||||
<% endpoint_params = "?v=#{item.id}" %>
|
<% endpoint_params = "?v=#{item.id}" %>
|
||||||
|
@ -102,7 +102,11 @@ private module Parsers
|
|||||||
premium = false
|
premium = false
|
||||||
|
|
||||||
premiere_timestamp = item_contents.dig?("upcomingEventData", "startTime").try { |t| Time.unix(t.as_s.to_i64) }
|
premiere_timestamp = item_contents.dig?("upcomingEventData", "startTime").try { |t| Time.unix(t.as_s.to_i64) }
|
||||||
|
author_verified_badge = item_contents["ownerBadges"]?.try do |badges_array|
|
||||||
|
badges_array.as_a.find(&.dig("metadataBadgeRenderer", "tooltip").as_s.== "Verified")
|
||||||
|
end
|
||||||
|
|
||||||
|
author_verified = (author_verified_badge && author_verified_badge.size > 0)
|
||||||
item_contents["badges"]?.try &.as_a.each do |badge|
|
item_contents["badges"]?.try &.as_a.each do |badge|
|
||||||
b = badge["metadataBadgeRenderer"]
|
b = badge["metadataBadgeRenderer"]
|
||||||
case b["label"].as_s
|
case b["label"].as_s
|
||||||
@ -129,6 +133,7 @@ private module Parsers
|
|||||||
live_now: live_now,
|
live_now: live_now,
|
||||||
premium: premium,
|
premium: premium,
|
||||||
premiere_timestamp: premiere_timestamp,
|
premiere_timestamp: premiere_timestamp,
|
||||||
|
author_verified: author_verified || false,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -156,7 +161,11 @@ private module Parsers
|
|||||||
private def self.parse(item_contents, author_fallback)
|
private def self.parse(item_contents, author_fallback)
|
||||||
author = extract_text(item_contents["title"]) || author_fallback.name
|
author = extract_text(item_contents["title"]) || author_fallback.name
|
||||||
author_id = item_contents["channelId"]?.try &.as_s || author_fallback.id
|
author_id = item_contents["channelId"]?.try &.as_s || author_fallback.id
|
||||||
|
author_verified_badge = item_contents["ownerBadges"]?.try do |badges_array|
|
||||||
|
badges_array.as_a.find(&.dig("metadataBadgeRenderer", "tooltip").as_s.== "Verified")
|
||||||
|
end
|
||||||
|
|
||||||
|
author_verified = (author_verified_badge && author_verified_badge.size > 0)
|
||||||
author_thumbnail = HelperExtractors.get_thumbnails(item_contents)
|
author_thumbnail = HelperExtractors.get_thumbnails(item_contents)
|
||||||
# When public subscriber count is disabled, the subscriberCountText isn't sent by InnerTube.
|
# When public subscriber count is disabled, the subscriberCountText isn't sent by InnerTube.
|
||||||
# Always simpleText
|
# Always simpleText
|
||||||
@ -179,6 +188,7 @@ private module Parsers
|
|||||||
video_count: video_count,
|
video_count: video_count,
|
||||||
description_html: description_html,
|
description_html: description_html,
|
||||||
auto_generated: auto_generated,
|
auto_generated: auto_generated,
|
||||||
|
author_verified: author_verified || false,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -206,18 +216,23 @@ private module Parsers
|
|||||||
private def self.parse(item_contents, author_fallback)
|
private def self.parse(item_contents, author_fallback)
|
||||||
title = extract_text(item_contents["title"]) || ""
|
title = extract_text(item_contents["title"]) || ""
|
||||||
plid = item_contents["playlistId"]?.try &.as_s || ""
|
plid = item_contents["playlistId"]?.try &.as_s || ""
|
||||||
|
author_verified_badge = item_contents["ownerBadges"]?.try do |badges_array|
|
||||||
|
badges_array.as_a.find(&.dig("metadataBadgeRenderer", "tooltip").as_s.== "Verified")
|
||||||
|
end
|
||||||
|
|
||||||
|
author_verified = (author_verified_badge && author_verified_badge.size > 0)
|
||||||
video_count = HelperExtractors.get_video_count(item_contents)
|
video_count = HelperExtractors.get_video_count(item_contents)
|
||||||
playlist_thumbnail = HelperExtractors.get_thumbnails(item_contents)
|
playlist_thumbnail = HelperExtractors.get_thumbnails(item_contents)
|
||||||
|
|
||||||
SearchPlaylist.new({
|
SearchPlaylist.new({
|
||||||
title: title,
|
title: title,
|
||||||
id: plid,
|
id: plid,
|
||||||
author: author_fallback.name,
|
author: author_fallback.name,
|
||||||
ucid: author_fallback.id,
|
ucid: author_fallback.id,
|
||||||
video_count: video_count,
|
video_count: video_count,
|
||||||
videos: [] of SearchPlaylistVideo,
|
videos: [] of SearchPlaylistVideo,
|
||||||
thumbnail: playlist_thumbnail,
|
thumbnail: playlist_thumbnail,
|
||||||
|
author_verified: author_verified || false,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -251,7 +266,11 @@ private module Parsers
|
|||||||
author_info = item_contents.dig?("shortBylineText", "runs", 0)
|
author_info = item_contents.dig?("shortBylineText", "runs", 0)
|
||||||
author = author_info.try &.["text"].as_s || author_fallback.name
|
author = author_info.try &.["text"].as_s || author_fallback.name
|
||||||
author_id = author_info.try { |x| HelperExtractors.get_browse_id(x) } || author_fallback.id
|
author_id = author_info.try { |x| HelperExtractors.get_browse_id(x) } || author_fallback.id
|
||||||
|
author_verified_badge = item_contents["ownerBadges"]?.try do |badges_array|
|
||||||
|
badges_array.as_a.find(&.dig("metadataBadgeRenderer", "tooltip").as_s.== "Verified")
|
||||||
|
end
|
||||||
|
|
||||||
|
author_verified = (author_verified_badge && author_verified_badge.size > 0)
|
||||||
videos = item_contents["videos"]?.try &.as_a.map do |v|
|
videos = item_contents["videos"]?.try &.as_a.map do |v|
|
||||||
v = v["childVideoRenderer"]
|
v = v["childVideoRenderer"]
|
||||||
v_title = v.dig?("title", "simpleText").try &.as_s || ""
|
v_title = v.dig?("title", "simpleText").try &.as_s || ""
|
||||||
@ -267,13 +286,14 @@ private module Parsers
|
|||||||
# TODO: item_contents["publishedTimeText"]?
|
# TODO: item_contents["publishedTimeText"]?
|
||||||
|
|
||||||
SearchPlaylist.new({
|
SearchPlaylist.new({
|
||||||
title: title,
|
title: title,
|
||||||
id: plid,
|
id: plid,
|
||||||
author: author,
|
author: author,
|
||||||
ucid: author_id,
|
ucid: author_id,
|
||||||
video_count: video_count,
|
video_count: video_count,
|
||||||
videos: videos,
|
videos: videos,
|
||||||
thumbnail: playlist_thumbnail,
|
thumbnail: playlist_thumbnail,
|
||||||
|
author_verified: author_verified || false,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user