package me.kavin.piped.utils; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.node.ObjectNode; import com.grack.nanojson.JsonObject; import com.grack.nanojson.JsonWriter; import com.rometools.rome.feed.synd.*; import com.rometools.rome.io.FeedException; import com.rometools.rome.io.SyndFeedOutput; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectArrayList; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.JoinType; import jakarta.persistence.criteria.Root; import me.kavin.piped.consts.Constants; import me.kavin.piped.ipfs.IPFS; import me.kavin.piped.utils.obj.*; import me.kavin.piped.utils.obj.db.PlaylistVideo; import me.kavin.piped.utils.obj.db.PubSub; import me.kavin.piped.utils.obj.db.User; import me.kavin.piped.utils.obj.db.Video; import me.kavin.piped.utils.obj.search.SearchChannel; import me.kavin.piped.utils.obj.search.SearchPlaylist; import me.kavin.piped.utils.resp.*; import okhttp3.FormBody; import okhttp3.Request; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.hibernate.Session; import org.hibernate.StatelessSession; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage; import org.schabi.newpipe.extractor.Page; import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; import org.schabi.newpipe.extractor.comments.CommentsInfo; import org.schabi.newpipe.extractor.comments.CommentsInfoItem; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.kiosk.KioskExtractor; import org.schabi.newpipe.extractor.kiosk.KioskInfo; import org.schabi.newpipe.extractor.kiosk.KioskList; import org.schabi.newpipe.extractor.localization.ContentCountry; import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.extractor.search.SearchInfo; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.utils.JsonUtils; import org.springframework.security.crypto.argon2.Argon2PasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import java.io.IOException; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static java.nio.charset.StandardCharsets.UTF_8; import static me.kavin.piped.consts.Constants.YOUTUBE_SERVICE; import static me.kavin.piped.consts.Constants.mapper; import static me.kavin.piped.utils.URLUtils.*; import static org.schabi.newpipe.extractor.NewPipe.getPreferredContentCountry; import static org.schabi.newpipe.extractor.NewPipe.getPreferredLocalization; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getJsonPostResponse; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.prepareDesktopJsonBuilder; public class ResponseHelper { public static byte[] streamsResponse(String videoId) throws Exception { final var futureStream = Multithreading.supplyAsync(() -> { try { return StreamInfo.getInfo("https://www.youtube.com/watch?v=" + videoId); } catch (Exception e) { ExceptionUtils.rethrow(e); } return null; }); final var futureLbryId = Multithreading.supplyAsync(() -> { try { return LbryHelper.getLBRYId(videoId); } catch (Exception e) { ExceptionHandler.handle(e); } return null; }); final var futureLBRY = Multithreading.supplyAsync(() -> { try { String lbryId = futureLbryId.get(2, TimeUnit.SECONDS); return LbryHelper.getLBRYStreamURL(lbryId); } catch (Exception e) { ExceptionHandler.handle(e); } return null; }); final var futureDislikeRating = Multithreading.supplyAsync(() -> { try { return RydHelper.getDislikeRating(videoId); } catch (Exception e) { ExceptionHandler.handle(e); } return null; }); final List subtitles = new ObjectArrayList<>(); final List chapters = new ObjectArrayList<>(); final StreamInfo info = futureStream.get(); info.getStreamSegments().forEach(segment -> chapters.add(new ChapterSegment(segment.getTitle(), rewriteURL(segment.getPreviewUrl()), segment.getStartTimeSeconds()))); info.getSubtitles() .forEach(subtitle -> subtitles.add(new Subtitle(rewriteURL(subtitle.getUrl()), subtitle.getFormat().getMimeType(), subtitle.getDisplayLanguageName(), subtitle.getLanguageTag(), subtitle.isAutoGenerated()))); final List videoStreams = new ObjectArrayList<>(); final List audioStreams = new ObjectArrayList<>(); String lbryURL = null; try { lbryURL = futureLBRY.get(3, TimeUnit.SECONDS); } catch (Exception e) { // ignored } if (lbryURL != null) videoStreams.add(new PipedStream(lbryURL, "MP4", "LBRY", "video/mp4", false)); boolean livestream = info.getStreamType() == StreamType.LIVE_STREAM; if (!livestream) { info.getVideoOnlyStreams().forEach(stream -> videoStreams.add(new PipedStream(rewriteVideoURL(stream.getUrl()), String.valueOf(stream.getFormat()), stream.getResolution(), stream.getFormat().getMimeType(), true, stream.getBitrate(), stream.getInitStart(), stream.getInitEnd(), stream.getIndexStart(), stream.getIndexEnd(), stream.getCodec(), stream.getWidth(), stream.getHeight(), 30))); info.getVideoStreams() .forEach(stream -> videoStreams .add(new PipedStream(rewriteVideoURL(stream.getUrl()), String.valueOf(stream.getFormat()), stream.getResolution(), stream.getFormat().getMimeType(), false))); info.getAudioStreams() .forEach(stream -> audioStreams.add(new PipedStream(rewriteVideoURL(stream.getUrl()), String.valueOf(stream.getFormat()), stream.getAverageBitrate() + " kbps", stream.getFormat().getMimeType(), false, stream.getBitrate(), stream.getInitStart(), stream.getInitEnd(), stream.getIndexStart(), stream.getIndexEnd(), stream.getCodec()))); } final List relatedStreams = new ObjectArrayList<>(); info.getRelatedItems().forEach(o -> { if (o instanceof StreamInfoItem) relatedStreams.add(collectRelatedStream(o)); }); long time = info.getUploadDate() != null ? info.getUploadDate().offsetDateTime().toInstant().toEpochMilli() : System.currentTimeMillis(); if (info.getUploadDate() != null && System.currentTimeMillis() - time < TimeUnit.DAYS.toMillis(Constants.FEED_RETENTION)) updateVideo(info.getId(), info, time); String lbryId; try { lbryId = futureLbryId.get(2, TimeUnit.SECONDS); } catch (Exception e) { lbryId = null; } // Attempt to get dislikes calculating with the RYD API rating if (info.getDislikeCount() < 0 && info.getLikeCount() >= 0) { double rating; try { rating = futureDislikeRating.get(2, TimeUnit.SECONDS); } catch (Exception e) { rating = -1; } if (rating > 1 && rating <= 5) { info.setDislikeCount(Math.round(info.getLikeCount() * ((5 - rating) / (rating - 1)))); } } final Streams streams = new Streams(info.getName(), info.getDescription().getContent(), info.getTextualUploadDate(), info.getUploaderName(), substringYouTube(info.getUploaderUrl()), rewriteURL(info.getUploaderAvatarUrl()), rewriteURL(info.getThumbnailUrl()), info.getDuration(), info.getViewCount(), info.getLikeCount(), info.getDislikeCount(), info.getUploaderSubscriberCount(), info.isUploaderVerified(), audioStreams, videoStreams, relatedStreams, subtitles, livestream, rewriteVideoURL(info.getHlsUrl()), rewriteVideoURL(info.getDashMpdUrl()), lbryId, chapters); return mapper.writeValueAsBytes(streams); } public static byte[] resolveClipId(String clipId) throws Exception { final byte[] body = JsonWriter.string(prepareDesktopJsonBuilder( getPreferredLocalization(), getPreferredContentCountry()) .value("url", "https://www.youtube.com/clip/" + clipId) .done()) .getBytes(UTF_8); final JsonObject jsonResponse = getJsonPostResponse("navigation/resolve_url", body, getPreferredLocalization()); final String videoId = JsonUtils.getString(jsonResponse, "endpoint.watchEndpoint.videoId"); return mapper.writeValueAsBytes(new VideoResolvedResponse(videoId)); } public static byte[] channelResponse(String channelPath) throws Exception { final ChannelInfo info = ChannelInfo.getInfo("https://youtube.com/" + channelPath); final List relatedStreams = new ObjectArrayList<>(); info.getRelatedItems().forEach(o -> relatedStreams.add(collectRelatedStream(o))); Multithreading.runAsync(() -> { me.kavin.piped.utils.obj.db.Channel channel = DatabaseHelper.getChannelFromId(info.getId()); try (Session s = DatabaseSessionFactory.createSession()) { if (channel != null) { if (channel.isVerified() != info.isVerified() || !channel.getUploaderAvatar().equals(info.getAvatarUrl())) { channel.setVerified(info.isVerified()); channel.setUploaderAvatar(info.getAvatarUrl()); var tr = s.beginTransaction(); s.merge(channel); tr.commit(); } for (StreamInfoItem item : info.getRelatedItems()) { long time = item.getUploadDate() != null ? item.getUploadDate().offsetDateTime().toInstant().toEpochMilli() : System.currentTimeMillis(); if (System.currentTimeMillis() - time < TimeUnit.DAYS.toMillis(Constants.FEED_RETENTION)) try { String id = YOUTUBE_SERVICE.getStreamLHFactory().getId(item.getUrl()); updateVideo(id, item, time, true); } catch (Exception e) { ExceptionHandler.handle(e); } } } } }); String nextpage = null; if (info.hasNextPage()) { Page page = info.getNextPage(); nextpage = mapper.writeValueAsString(page); } final Channel channel = new Channel(info.getId(), info.getName(), rewriteURL(info.getAvatarUrl()), rewriteURL(info.getBannerUrl()), info.getDescription(), info.getSubscriberCount(), info.isVerified(), nextpage, relatedStreams); IPFS.publishData(channel); return mapper.writeValueAsBytes(channel); } public static byte[] channelPageResponse(String channelId, String prevpageStr) throws IOException, ExtractionException { if (StringUtils.isEmpty(prevpageStr)) return mapper.writeValueAsBytes(new InvalidRequestResponse()); Page prevpage = mapper.readValue(prevpageStr, Page.class); InfoItemsPage info = ChannelInfo.getMoreItems(YOUTUBE_SERVICE, "https://youtube.com/channel/" + channelId, prevpage); final List relatedStreams = new ObjectArrayList<>(); info.getItems().forEach(o -> relatedStreams.add(collectRelatedStream(o))); String nextpage = null; if (info.hasNextPage()) { Page page = info.getNextPage(); nextpage = mapper.writeValueAsString(page); } final StreamsPage streamspage = new StreamsPage(nextpage, relatedStreams); return mapper.writeValueAsBytes(streamspage); } public static byte[] trendingResponse(String region) throws ExtractionException, IOException { if (region == null) return mapper.writeValueAsBytes(new InvalidRequestResponse()); final List relatedStreams = new ObjectArrayList<>(); KioskList kioskList = YOUTUBE_SERVICE.getKioskList(); kioskList.forceContentCountry(new ContentCountry(region)); KioskExtractor extractor = kioskList.getDefaultKioskExtractor(); extractor.fetchPage(); KioskInfo info = KioskInfo.getInfo(extractor); info.getRelatedItems().forEach(o -> relatedStreams.add(collectRelatedStream(o))); return mapper.writeValueAsBytes(relatedStreams); } public static byte[] playlistResponse(String playlistId) throws ExtractionException, IOException { if (StringUtils.isBlank(playlistId)) return mapper.writeValueAsBytes(new InvalidRequestResponse()); if (playlistId.matches("[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}")) return playlistPipedResponse(playlistId); return playlistYouTubeResponse(playlistId); } private static byte[] playlistPipedResponse(String playlistId) throws IOException { try (StatelessSession s = DatabaseSessionFactory.createStatelessSession()) { var cb = s.getCriteriaBuilder(); var cq = cb.createQuery(me.kavin.piped.utils.obj.db.Playlist.class); var root = cq.from(me.kavin.piped.utils.obj.db.Playlist.class); root.fetch("videos", JoinType.LEFT) .fetch("channel", JoinType.LEFT); root.fetch("owner", JoinType.LEFT); cq.select(root); cq.where(cb.equal(root.get("playlist_id"), UUID.fromString(playlistId))); var query = s.createQuery(cq); var pl = query.uniqueResult(); if (pl == null) return mapper.writeValueAsBytes(mapper.createObjectNode() .put("error", "Playlist not found")); final List relatedStreams = new ObjectArrayList<>(); var videos = pl.getVideos(); for (var video : videos) { var channel = video.getChannel(); relatedStreams.add(new StreamItem("/watch?v=" + video.getId(), video.getTitle(), rewriteURL(video.getThumbnail()), channel.getUploader(), "/channel/" + channel.getUploaderId(), rewriteURL(channel.getUploaderAvatar()), null, null, video.getDuration(), -1, -1, channel.isVerified())); } final Playlist playlist = new Playlist(pl.getName(), rewriteURL(pl.getThumbnail()), null, null, pl.getOwner().getUsername(), null, null, videos.size(), relatedStreams); return mapper.writeValueAsBytes(playlist); } } private static byte[] playlistYouTubeResponse(String playlistId) throws IOException, ExtractionException { final PlaylistInfo info = PlaylistInfo.getInfo("https://www.youtube.com/playlist?list=" + playlistId); final List relatedStreams = new ObjectArrayList<>(); info.getRelatedItems().forEach(o -> relatedStreams.add(collectRelatedStream(o))); String nextpage = null; if (info.hasNextPage()) { Page page = info.getNextPage(); nextpage = mapper.writeValueAsString(page); } final Playlist playlist = new Playlist(info.getName(), rewriteURL(info.getThumbnailUrl()), rewriteURL(info.getBannerUrl()), nextpage, info.getUploaderName().isEmpty() ? null : info.getUploaderName(), substringYouTube(info.getUploaderUrl()), rewriteURL(info.getUploaderAvatarUrl()), (int) info.getStreamCount(), relatedStreams); return mapper.writeValueAsBytes(playlist); } public static byte[] playlistPageResponse(String playlistId, String prevpageStr) throws IOException, ExtractionException { if (StringUtils.isEmpty(prevpageStr)) return mapper.writeValueAsBytes(new InvalidRequestResponse()); Page prevpage = mapper.readValue(prevpageStr, Page.class); InfoItemsPage info = PlaylistInfo.getMoreItems(YOUTUBE_SERVICE, "https://www.youtube.com/playlist?list=" + playlistId, prevpage); final List relatedStreams = new ObjectArrayList<>(); info.getItems().forEach(o -> relatedStreams.add(collectRelatedStream(o))); String nextpage = null; if (info.hasNextPage()) { Page page = info.getNextPage(); nextpage = mapper.writeValueAsString(page); } final StreamsPage streamspage = new StreamsPage(nextpage, relatedStreams); return mapper.writeValueAsBytes(streamspage); } public static byte[] playlistRSSResponse(String playlistId) throws ExtractionException, IOException, FeedException { if (StringUtils.isBlank(playlistId)) return mapper.writeValueAsBytes(new InvalidRequestResponse()); if (playlistId.matches("[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}")) return playlistPipedRSSResponse(playlistId); return playlistYouTubeRSSResponse(playlistId); } private static byte[] playlistPipedRSSResponse(String playlistId) throws FeedException { try (StatelessSession s = DatabaseSessionFactory.createStatelessSession()) { var cb = s.getCriteriaBuilder(); var cq = cb.createQuery(me.kavin.piped.utils.obj.db.Playlist.class); var root = cq.from(me.kavin.piped.utils.obj.db.Playlist.class); root.fetch("videos", JoinType.LEFT) .fetch("channel", JoinType.LEFT); root.fetch("owner", JoinType.LEFT); cq.select(root); cq.where(cb.equal(root.get("playlist_id"), UUID.fromString(playlistId))); var query = s.createQuery(cq); var pl = query.uniqueResult(); final List entries = new ObjectArrayList<>(); SyndFeed feed = new SyndFeedImpl(); feed.setFeedType("rss_2.0"); feed.setTitle(pl.getName()); feed.setAuthor(pl.getOwner().getUsername()); feed.setDescription(String.format("%s - Piped", pl.getName())); feed.setLink(Constants.FRONTEND_URL + "/playlist?list=" + pl.getPlaylistId()); feed.setPublishedDate(new Date()); for (var video : pl.getVideos()) { SyndEntry entry = new SyndEntryImpl(); entry.setAuthor(video.getChannel().getUploader()); entry.setLink(Constants.FRONTEND_URL + "/video?id=" + video.getId()); entry.setUri(Constants.FRONTEND_URL + "/video?id=" + video.getId()); entry.setTitle(video.getTitle()); entries.add(entry); } feed.setEntries(entries); return new SyndFeedOutput().outputString(feed).getBytes(UTF_8); } } private static byte[] playlistYouTubeRSSResponse(String playlistId) throws IOException, ExtractionException, FeedException { final PlaylistInfo info = PlaylistInfo.getInfo("https://www.youtube.com/playlist?list=" + playlistId); final List entries = new ObjectArrayList<>(); SyndFeed feed = new SyndFeedImpl(); feed.setFeedType("rss_2.0"); feed.setTitle(info.getName()); feed.setAuthor(info.getUploaderName()); feed.setDescription(String.format("%s - Piped", info.getName())); feed.setLink(Constants.FRONTEND_URL + substringYouTube(info.getUrl())); feed.setPublishedDate(new Date()); info.getRelatedItems().forEach(item -> { SyndEntry entry = new SyndEntryImpl(); entry.setAuthor(item.getUploaderName()); entry.setLink(item.getUrl()); entry.setUri(item.getUrl()); entry.setTitle(item.getName()); entries.add(entry); }); feed.setEntries(entries); return new SyndFeedOutput().outputString(feed).getBytes(UTF_8); } public static byte[] suggestionsResponse(String query) throws IOException, ExtractionException { if (StringUtils.isEmpty(query)) return mapper.writeValueAsBytes(new InvalidRequestResponse()); return mapper.writeValueAsBytes(YOUTUBE_SERVICE.getSuggestionExtractor().suggestionList(query)); } public static byte[] opensearchSuggestionsResponse(String query) throws IOException, ExtractionException { if (StringUtils.isEmpty(query)) return mapper.writeValueAsBytes(new InvalidRequestResponse()); return mapper.writeValueAsBytes(Arrays.asList( query, YOUTUBE_SERVICE.getSuggestionExtractor().suggestionList(query) )); } public static byte[] searchResponse(String q, String filter) throws IOException, ExtractionException { final SearchInfo info = SearchInfo.getInfo(YOUTUBE_SERVICE, YOUTUBE_SERVICE.getSearchQHFactory().fromQuery(q, Collections.singletonList(filter), null)); ObjectArrayList items = new ObjectArrayList<>(); info.getRelatedItems().forEach(item -> { switch (item.getInfoType()) { case STREAM: items.add(collectRelatedStream(item)); break; case CHANNEL: ChannelInfoItem channel = (ChannelInfoItem) item; items.add(new SearchChannel(item.getName(), rewriteURL(item.getThumbnailUrl()), substringYouTube(item.getUrl()), channel.getDescription(), channel.getSubscriberCount(), channel.getStreamCount(), channel.isVerified())); break; case PLAYLIST: PlaylistInfoItem playlist = (PlaylistInfoItem) item; items.add(new SearchPlaylist(item.getName(), rewriteURL(item.getThumbnailUrl()), substringYouTube(item.getUrl()), playlist.getUploaderName(), playlist.getStreamCount())); break; default: break; } }); Page nextpage = info.getNextPage(); return mapper.writeValueAsBytes(new SearchResults(items, mapper.writeValueAsString(nextpage), info.getSearchSuggestion(), info.isCorrectedSearch())); } public static byte[] searchPageResponse(String q, String filter, String prevpageStr) throws IOException, ExtractionException { if (StringUtils.isEmpty(prevpageStr)) return mapper.writeValueAsBytes(new InvalidRequestResponse()); Page prevpage = mapper.readValue(prevpageStr, Page.class); InfoItemsPage pages = SearchInfo.getMoreItems(YOUTUBE_SERVICE, YOUTUBE_SERVICE.getSearchQHFactory().fromQuery(q, Collections.singletonList(filter), null), prevpage); ObjectArrayList items = new ObjectArrayList<>(); pages.getItems().forEach(item -> { switch (item.getInfoType()) { case STREAM: items.add(collectRelatedStream(item)); break; case CHANNEL: ChannelInfoItem channel = (ChannelInfoItem) item; items.add(new SearchChannel(item.getName(), rewriteURL(item.getThumbnailUrl()), substringYouTube(item.getUrl()), channel.getDescription(), channel.getSubscriberCount(), channel.getStreamCount(), channel.isVerified())); break; case PLAYLIST: PlaylistInfoItem playlist = (PlaylistInfoItem) item; items.add(new SearchPlaylist(item.getName(), rewriteURL(item.getThumbnailUrl()), substringYouTube(item.getUrl()), playlist.getUploaderName(), playlist.getStreamCount())); break; default: break; } }); Page nextpage = pages.getNextPage(); return mapper .writeValueAsBytes(new SearchResults(items, mapper.writeValueAsString(nextpage))); } public static byte[] commentsResponse(String videoId) throws Exception { CommentsInfo info = CommentsInfo.getInfo("https://www.youtube.com/watch?v=" + videoId); List comments = new ObjectArrayList<>(); info.getRelatedItems().forEach(comment -> { try { String repliespage = null; if (comment.getReplies() != null) repliespage = mapper.writeValueAsString(comment.getReplies()); comments.add(new Comment(comment.getUploaderName(), rewriteURL(comment.getUploaderAvatarUrl()), comment.getCommentId(), comment.getCommentText(), comment.getTextualUploadDate(), substringYouTube(comment.getUploaderUrl()), repliespage, comment.getLikeCount(), comment.isHeartedByUploader(), comment.isPinned(), comment.isUploaderVerified())); } catch (JsonProcessingException e) { ExceptionHandler.handle(e); } }); String nextpage = null; if (info.hasNextPage()) { Page page = info.getNextPage(); nextpage = mapper.writeValueAsString(page); } CommentsPage commentsItem = new CommentsPage(comments, nextpage, info.isCommentsDisabled()); return mapper.writeValueAsBytes(commentsItem); } public static byte[] commentsPageResponse(String videoId, String prevpageStr) throws Exception { if (StringUtils.isEmpty(prevpageStr)) return mapper.writeValueAsBytes(new InvalidRequestResponse()); Page prevpage = mapper.readValue(prevpageStr, Page.class); InfoItemsPage info = CommentsInfo.getMoreItems(YOUTUBE_SERVICE, "https://www.youtube.com/watch?v=" + videoId, prevpage); List comments = new ObjectArrayList<>(); info.getItems().forEach(comment -> { try { String repliespage = null; if (comment.getReplies() != null) repliespage = mapper.writeValueAsString(comment.getReplies()); comments.add(new Comment(comment.getUploaderName(), rewriteURL(comment.getUploaderAvatarUrl()), comment.getCommentId(), comment.getCommentText(), comment.getTextualUploadDate(), substringYouTube(comment.getUploaderUrl()), repliespage, comment.getLikeCount(), comment.isHeartedByUploader(), comment.isPinned(), comment.isUploaderVerified())); } catch (JsonProcessingException e) { ExceptionHandler.handle(e); } }); String nextpage = null; if (info.hasNextPage()) { Page page = info.getNextPage(); nextpage = mapper.writeValueAsString(page); } CommentsPage commentsItem = new CommentsPage(comments, nextpage, false); return mapper.writeValueAsBytes(commentsItem); } public static byte[] registerResponse(String user, String pass) throws IOException { if (Constants.DISABLE_REGISTRATION) return mapper.writeValueAsBytes(new DisabledRegistrationResponse()); if (StringUtils.isBlank(user) || StringUtils.isBlank(pass)) return mapper.writeValueAsBytes(new InvalidRequestResponse()); if (user.length() > 24) return mapper.writeValueAsBytes( mapper.createObjectNode() .put("error", "The username must be less than 24 characters") ); user = user.toLowerCase(); try (Session s = DatabaseSessionFactory.createSession()) { CriteriaBuilder cb = s.getCriteriaBuilder(); CriteriaQuery cr = cb.createQuery(User.class); Root root = cr.from(User.class); cr.select(root).where(cb.equal(root.get("username"), user)); boolean registered = s.createQuery(cr).uniqueResult() != null; if (registered) return mapper.writeValueAsBytes(new AlreadyRegisteredResponse()); if (Constants.COMPROMISED_PASSWORD_CHECK) { String sha1Hash = DigestUtils.sha1Hex(pass).toUpperCase(); String prefix = sha1Hash.substring(0, 5); String suffix = sha1Hash.substring(5); String[] entries = RequestUtils .sendGet("https://api.pwnedpasswords.com/range/" + prefix, "github.com/TeamPiped/Piped-Backend") .split("\n"); for (String entry : entries) if (StringUtils.substringBefore(entry, ":").equals(suffix)) return mapper.writeValueAsBytes(new CompromisedPasswordResponse()); } User newuser = new User(user, argon2PasswordEncoder.encode(pass), Set.of()); var tr = s.beginTransaction(); s.persist(newuser); tr.commit(); return mapper.writeValueAsBytes(new LoginResponse(newuser.getSessionId())); } } private static final Argon2PasswordEncoder argon2PasswordEncoder = new Argon2PasswordEncoder(); private static final BCryptPasswordEncoder bcryptPasswordEncoder = new BCryptPasswordEncoder(); private static boolean hashMatch(String hash, String pass) { return hash.startsWith("$argon2") ? argon2PasswordEncoder.matches(pass, hash) : bcryptPasswordEncoder.matches(pass, hash); } public static byte[] loginResponse(String user, String pass) throws IOException { if (user == null || pass == null) return mapper.writeValueAsBytes(new InvalidRequestResponse()); user = user.toLowerCase(); try (StatelessSession s = DatabaseSessionFactory.createStatelessSession()) { CriteriaBuilder cb = s.getCriteriaBuilder(); CriteriaQuery cr = cb.createQuery(User.class); Root root = cr.from(User.class); cr.select(root).where(root.get("username").in(user)); User dbuser = s.createQuery(cr).uniqueResult(); if (dbuser != null) { String hash = dbuser.getPassword(); if (hashMatch(hash, pass)) { return mapper.writeValueAsBytes(new LoginResponse(dbuser.getSessionId())); } } return mapper.writeValueAsBytes(new IncorrectCredentialsResponse()); } } public static byte[] deleteUserResponse(String session, String pass) throws IOException { if (StringUtils.isBlank(pass)) return mapper.writeValueAsBytes(new InvalidRequestResponse()); try (Session s = DatabaseSessionFactory.createSession()) { User user = DatabaseHelper.getUserFromSession(session); if (user == null) return mapper.writeValueAsBytes(new AuthenticationFailureResponse()); String hash = user.getPassword(); if (!hashMatch(hash, pass)) return mapper.writeValueAsBytes(new IncorrectCredentialsResponse()); try { var tr = s.beginTransaction(); s.remove(user); tr.commit(); } catch (Exception e) { return mapper.writeValueAsBytes(new ErrorResponse(ExceptionUtils.getStackTrace(e), e.getMessage())); } return mapper.writeValueAsBytes(new DeleteUserResponse(user.getUsername())); } } public static byte[] subscribeResponse(String session, String channelId) throws IOException { try (Session s = DatabaseSessionFactory.createSession()) { User user = DatabaseHelper.getUserFromSessionWithSubscribed(session); if (user != null) { if (!user.getSubscribed().contains(channelId)) { user.getSubscribed().add(channelId); var tr = s.beginTransaction(); s.merge(user); tr.commit(); Multithreading.runAsync(() -> { var channel = DatabaseHelper.getChannelFromId(channelId); if (channel == null) { Multithreading.runAsync(() -> saveChannel(channelId)); } }); } return mapper.writeValueAsBytes(new AcceptedResponse()); } return mapper.writeValueAsBytes(new AuthenticationFailureResponse()); } } public static byte[] unsubscribeResponse(String session, String channelId) throws IOException { User user = DatabaseHelper.getUserFromSession(session); if (user != null) { try (Session s = DatabaseSessionFactory.createSession()) { var tr = s.beginTransaction(); s.createNativeMutationQuery("delete from users_subscribed where subscriber = :id and channel = :channel") .setParameter("id", user.getId()).setParameter("channel", channelId).executeUpdate(); tr.commit(); return mapper.writeValueAsBytes(new AcceptedResponse()); } } return mapper.writeValueAsBytes(new AuthenticationFailureResponse()); } public static byte[] isSubscribedResponse(String session, String channelId) throws IOException { try (StatelessSession s = DatabaseSessionFactory.createStatelessSession()) { var cb = s.getCriteriaBuilder(); var query = cb.createQuery(Long.class); var root = query.from(User.class); query.select(cb.count(root)) .where(cb.and( cb.equal(root.get("sessionId"), session), cb.isMember(channelId, root.get("subscribed_ids")) )); var subscribed = s.createQuery(query).getSingleResult() > 0; return mapper.writeValueAsBytes(new SubscribeStatusResponse(subscribed)); } } public static byte[] feedResponse(String session) throws IOException { if (StringUtils.isBlank(session)) return mapper.writeValueAsBytes(new AuthenticationFailureResponse()); User user = DatabaseHelper.getUserFromSession(session); if (user != null) { try (StatelessSession s = DatabaseSessionFactory.createStatelessSession()) { CriteriaBuilder cb = s.getCriteriaBuilder(); // Get all videos from subscribed channels, with channel info CriteriaQuery