DUMMY.pm 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. package Trog::Data::DUMMY;
  2. use strict;
  3. use warnings;
  4. no warnings 'experimental';
  5. use feature qw{signatures};
  6. use Carp qw{confess};
  7. use JSON::MaybeXS;
  8. use File::Slurper;
  9. use File::Copy;
  10. use Mojo::File;
  11. use List::Util;
  12. =head1 WARNING
  13. Do not use this as a production data model. It is *not* safe to race conditions, and is only here for testing.
  14. =head1 QUERY FORMAT
  15. The $query_language and $query_help variables are presented to the user as to how to use the search box in the tCMS header.
  16. =cut
  17. our $datastore = 'data/DUMMY.json';
  18. our $query_language = 'Perl Regex in Quotemeta';
  19. our $query_help = 'https://perldoc.perl.org/functions/quotemeta.html';
  20. =head1 POST STRUCTURE
  21. Posts generally need to have the following:
  22. data: Brief description of content, or the content itself.
  23. content_type: What this content actually is. Used to filter into the appropriate pages.
  24. href: Primary link. This is the subject of a news post, or a link to the item itself. Can be local or remote.
  25. local_href: Backup link. Automatically created link to a static cache of the content.
  26. title: Title of the content. Used as link name for the 'href' attribute.
  27. user: User was banned for this post
  28. id: Internal identifier in datastore for the post.
  29. tags: array ref of appropriate tags.
  30. created: timestamp of creation of this version of the post
  31. version: revision # of this post.
  32. =cut
  33. =head1 CONSTRUCTOR
  34. =head2 new(Config::Simple $config)
  35. Try not to do expensive things here.
  36. =cut
  37. sub new ($class, $config) {
  38. $config = $config->vars();
  39. $config->{lang} = $query_language;
  40. $config->{help} = $query_help;
  41. return bless($config,__PACKAGE__);
  42. }
  43. =head1 METHODS
  44. =head2 get(%request)
  45. Queries the data model in the way a "real" data model module ought to.
  46. id => Filter down to just the post by ID. May be subsequently filtered by ACL, resulting in a 404 (which is good, as it does not disclose info).
  47. version => if id is passed, return the provided post version rather than the most recent one
  48. tags => ARRAYREF of tags, any one of which is required to give a result. If none are passed, no filtering is performed.
  49. acls => ARRAYREF of acl tags, any one of which is required to give result. Filter applies after tags. 'admin' ACL being present skips this filter.
  50. page => Offset multiplier for pagination.
  51. limit => Offset for pagination.
  52. like => Search query, as might be passed in the search bar.
  53. author => filter by post author
  54. =cut
  55. sub _read {
  56. confess "Can't find datastore!" unless -f $datastore;
  57. my $slurped = File::Slurper::read_text($datastore);
  58. return JSON::MaybeXS::decode_json($slurped);
  59. }
  60. sub _write($data) {
  61. open(my $fh, '>', $datastore) or confess;
  62. print $fh JSON::MaybeXS::encode_json($data);
  63. close $fh;
  64. }
  65. sub get ($self, %request) {
  66. my $example_posts = _read();
  67. $request{acls} //= [];
  68. $request{tags} //=[];
  69. my @filtered = @$example_posts;
  70. # If an ID is passed, just get that (and all it's prior versions
  71. if ($request{id}) {
  72. @filtered = grep { $_->{id} eq $request{id} } @filtered if $request{id};
  73. @filtered = _dedup_versions($request{version}, @filtered);
  74. @filtered = _add_post_type(@filtered);
  75. # Next, add the type of post this is
  76. @filtered = _add_media_type(@filtered);
  77. # Finally, add visibility
  78. @filtered = _add_visibility(@filtered);
  79. return (1, \@filtered);
  80. }
  81. @filtered = _dedup_versions(undef, @filtered);
  82. # Heal bad data
  83. @filtered = map { my $t = $_->{tags}; @$t = grep { defined $_ } @$t; $_ } @filtered;
  84. # Next, handle the query, tags and ACLs
  85. @filtered = grep { my $tags = $_->{tags}; grep { my $t = $_; grep {$t eq $_ } @{$request{tags}} } @$tags } @filtered if @{$request{tags}};
  86. @filtered = grep { my $tags = $_->{tags}; grep { my $t = $_; grep {$t eq $_ } @{$request{acls}} } @$tags } @filtered unless grep { $_ eq 'admin' } @{$request{acls}};
  87. @filtered = grep { $_->{data} =~ m/\Q$request{like}\E/i } @filtered if $request{like};
  88. @filtered = grep { $_->{user} eq $request{author} } @filtered if $request{author};
  89. # Finally, paginate
  90. my $offset = int($request{limit} // 25);
  91. $offset = @filtered < $offset ? @filtered : $offset;
  92. my $pages = int(scalar(@filtered) / ($offset || 1) );
  93. @filtered = splice(@filtered, ( int($request{page}) -1) * $offset, $offset) if $request{page} && $request{limit};
  94. # Next, go ahead and build the "post type"
  95. @filtered = _add_post_type(@filtered);
  96. # Next, add the type of post this is
  97. @filtered = _add_media_type(@filtered);
  98. # Finally, add visibility
  99. @filtered = _add_visibility(@filtered);
  100. return ($pages,\@filtered);
  101. }
  102. sub _dedup_versions ($version=-1, @posts) {
  103. if (defined $version) {
  104. my $version_max = List::Util::max(map { $_->{version } } @posts);
  105. return map {
  106. $_->{version_max} = $version_max;
  107. $_
  108. } grep { $_->{version} eq $version } @posts;
  109. }
  110. my @uniqids = List::Util::uniq(map { $_->{id} } @posts);
  111. my %posts_deduped;
  112. for my $id (@uniqids) {
  113. my @ofid = sort { $b->{version} cmp $a->{version} } grep { $_->{id} eq $id } @posts;
  114. my $version_max = List::Util::max(map { $_->{version } } @ofid);
  115. $posts_deduped{$id} = $ofid[0];
  116. $posts_deduped{$id}{version_max} = $version_max;
  117. }
  118. my @deduped = @posts_deduped{@uniqids};
  119. return @deduped;
  120. }
  121. =head2 total_posts() = INT $num
  122. Returns the total number of posts.
  123. Used to determine paginator parameters.
  124. =cut
  125. sub total_posts {
  126. my $example_posts = _read();
  127. return scalar(@$example_posts);
  128. }
  129. sub _add_post_type (@posts) {
  130. return map {
  131. my $post = $_;
  132. my $type = 'file';
  133. $type = 'blog' if grep { $_ eq 'blog' } @{$post->{tags}};
  134. $type = 'microblog' if grep { $_ eq 'news' } @{$post->{tags}};
  135. $type = 'profile' if grep { $_ eq 'about' } @{$post->{tags}};
  136. $type = 'series' if grep { $_ eq 'series' } @{$post->{tags}};
  137. $post->{type} = $type;
  138. $post
  139. } @posts;
  140. }
  141. sub _add_media_type (@posts) {
  142. return map {
  143. my $post = $_;
  144. $post->{content_type} //= '';
  145. $post->{is_video} = 1 if $post->{content_type} =~ m/^video\//;
  146. $post->{is_audio} = 1 if $post->{content_type} =~ m/^audio\//;
  147. $post->{is_image} = 1 if $post->{content_type} =~ m/^image\//;
  148. $post->{is_profile} = 1 if grep {$_ eq 'about' } @{$post->{tags}};
  149. $post
  150. } @posts;
  151. }
  152. sub _add_visibility (@posts) {
  153. return map {
  154. my $post = $_;
  155. my @visibilities = grep { my $tag = $_; grep { $_ eq $tag } qw{private unlisted public} } @{$post->{tags}};
  156. $post->{visibility} = $visibilities[0];
  157. $post
  158. } @posts;
  159. }
  160. =head2 add(@posts) = BOOL $failed_or_not
  161. Add the provided posts to the datastore.
  162. If any post already exists with the same id, a new post with a version higher than it will be added.
  163. =cut
  164. sub add ($self, @posts) {
  165. require UUID::Tiny;
  166. my $example_posts = _read();
  167. foreach my $post (@posts) {
  168. $post->{id} //= UUID::Tiny::create_uuid_as_string(UUID::Tiny::UUID_V1, UUID::Tiny::UUID_NS_DNS);
  169. $post->{created} = time();
  170. my (undef, $existing_posts) = $self->get( id => $post->{id} );
  171. if (@$existing_posts) {
  172. my $existing_post = $existing_posts->[0];
  173. $post->{version} = $existing_post->{version};
  174. $post->{version}++;
  175. }
  176. $post->{version} //= 0;
  177. $post = _process($post);
  178. push @$example_posts, $post;
  179. }
  180. _write($example_posts);
  181. return 0;
  182. }
  183. # Not actually a subprocess, kek
  184. sub _process ($post) {
  185. $post->{href} = _handle_upload($post->{file}, $post->{id}) if $post->{file};
  186. $post->{preview} = _handle_upload($post->{preview_file}, $post->{id}) if $post->{preview_file};
  187. $post->{wallpaper} = _handle_upload($post->{wallpaper_file}, $post->{id}) if $post->{wallpaper_file};
  188. $post->{preview} = $post->{href} if $post->{app} eq 'image';
  189. delete $post->{app};
  190. delete $post->{file};
  191. delete $post->{preview_file};
  192. delete $post->{route};
  193. delete $post->{domain};
  194. # Handle acls/tags
  195. $post->{tags} //= [];
  196. @{$post->{tags}} = grep { my $subj = $_; !grep { $_ eq $subj} qw{public private unlisted} } @{$post->{tags}};
  197. push(@{$post->{tags}}, delete $post->{acls}) if $post->{visibility} eq 'private';
  198. push(@{$post->{tags}}, delete $post->{visibility});
  199. #Filter adding the same acl twice
  200. @{$post->{tags}} = List::Util::uniq(@{$post->{tags}});
  201. # Handle multimedia content types
  202. if ($post->{href}) {
  203. my $mf = Mojo::File->new("www/$post->{href}");
  204. my $ext = '.'.$mf->extname();
  205. $post->{content_type} = Plack::MIME->mime_type($ext) if $ext;
  206. }
  207. if ($post->{video_href}) {
  208. my $mf = Mojo::File->new("www/$post->{video_href}");
  209. my $ext = '.'.$mf->extname();
  210. $post->{video_content_type} = Plack::MIME->mime_type($ext) if $ext;
  211. }
  212. if ($post->{audio_href}) {
  213. my $mf = Mojo::File->new("www/$post->{audio_href}");
  214. my $ext = '.'.$mf->extname();
  215. $post->{audio_content_type} = Plack::MIME->mime_type($ext) if $ext;
  216. }
  217. return $post;
  218. }
  219. sub _handle_upload ($file, $uuid) {
  220. my $f = $file->{tempname};
  221. my $newname = "$uuid.$file->{filename}";
  222. File::Copy::move($f, "www/assets/$newname");
  223. return "/assets/$newname";
  224. }
  225. =head2 delete(@posts)
  226. Delete the following posts.
  227. Will remove all versions of said post.
  228. =cut
  229. sub delete($self, @posts) {
  230. my $example_posts = _read();
  231. foreach my $update (@posts) {
  232. @$example_posts = grep { $_->{id} ne $update->{id} } @$example_posts;
  233. }
  234. _write($example_posts);
  235. return 0;
  236. }
  237. 1;