001/*
002 * PlotSquared, a land and world management plugin for Minecraft.
003 * Copyright (C) IntellectualSites <https://intellectualsites.com>
004 * Copyright (C) IntellectualSites team and contributors
005 *
006 * This program is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 *
011 * This program is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
014 * GNU General Public License for more details.
015 *
016 * You should have received a copy of the GNU General Public License
017 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
018 */
019package com.plotsquared.core.plot.expiration;
020
021import com.google.inject.Inject;
022import com.plotsquared.core.PlotPlatform;
023import com.plotsquared.core.PlotSquared;
024import com.plotsquared.core.configuration.caption.Caption;
025import com.plotsquared.core.configuration.caption.Templates;
026import com.plotsquared.core.configuration.caption.TranslatableCaption;
027import com.plotsquared.core.database.DBFunc;
028import com.plotsquared.core.events.PlotFlagAddEvent;
029import com.plotsquared.core.events.PlotUnlinkEvent;
030import com.plotsquared.core.events.Result;
031import com.plotsquared.core.player.MetaDataAccess;
032import com.plotsquared.core.player.OfflinePlotPlayer;
033import com.plotsquared.core.player.PlayerMetaDataKeys;
034import com.plotsquared.core.player.PlotPlayer;
035import com.plotsquared.core.plot.Plot;
036import com.plotsquared.core.plot.PlotArea;
037import com.plotsquared.core.plot.PlotAreaType;
038import com.plotsquared.core.plot.flag.GlobalFlagContainer;
039import com.plotsquared.core.plot.flag.PlotFlag;
040import com.plotsquared.core.plot.flag.implementations.AnalysisFlag;
041import com.plotsquared.core.plot.flag.implementations.KeepFlag;
042import com.plotsquared.core.plot.flag.implementations.ServerPlotFlag;
043import com.plotsquared.core.util.EventDispatcher;
044import com.plotsquared.core.util.query.PlotQuery;
045import com.plotsquared.core.util.task.RunnableVal;
046import com.plotsquared.core.util.task.RunnableVal3;
047import com.plotsquared.core.util.task.TaskManager;
048import com.plotsquared.core.util.task.TaskTime;
049import net.kyori.adventure.text.minimessage.Template;
050import org.checkerframework.checker.nullness.qual.NonNull;
051
052import java.util.ArrayDeque;
053import java.util.ArrayList;
054import java.util.Collection;
055import java.util.Collections;
056import java.util.HashSet;
057import java.util.Iterator;
058import java.util.Objects;
059import java.util.UUID;
060import java.util.concurrent.ConcurrentHashMap;
061import java.util.concurrent.ConcurrentLinkedDeque;
062
063public class ExpireManager {
064
065    /**
066     * @deprecated Use {@link PlotPlatform#expireManager()} instead
067     */
068    @Deprecated(forRemoval = true, since = "6.10.2")
069    public static ExpireManager IMP;
070    private final ConcurrentHashMap<UUID, Long> dates_cache;
071    private final ConcurrentHashMap<UUID, Long> account_age_cache;
072    private final EventDispatcher eventDispatcher;
073    private final ArrayDeque<ExpiryTask> tasks;
074    private volatile HashSet<Plot> plotsToDelete;
075    /**
076     * 0 = stopped, 1 = stopping, 2 = running
077     */
078    private int running;
079
080    @Inject
081    public ExpireManager(final @NonNull EventDispatcher eventDispatcher) {
082        this.tasks = new ArrayDeque<>();
083        this.dates_cache = new ConcurrentHashMap<>();
084        this.account_age_cache = new ConcurrentHashMap<>();
085        this.eventDispatcher = eventDispatcher;
086    }
087
088    public void addTask(ExpiryTask task) {
089        this.tasks.add(task);
090    }
091
092    public void handleJoin(PlotPlayer<?> pp) {
093        storeDate(pp.getUUID(), System.currentTimeMillis());
094        if (plotsToDelete != null && !plotsToDelete.isEmpty()) {
095            for (Plot plot : pp.getPlots()) {
096                plotsToDelete.remove(plot);
097            }
098        }
099        confirmExpiry(pp);
100    }
101
102    public void handleEntry(PlotPlayer<?> pp, Plot plot) {
103        if (plotsToDelete != null && !plotsToDelete.isEmpty() && pp
104                .hasPermission("plots.admin.command.autoclear") && plotsToDelete.contains(plot)) {
105            if (!isExpired(new ArrayDeque<>(tasks), plot).isEmpty()) {
106                confirmExpiry(pp);
107            } else {
108                plotsToDelete.remove(plot);
109                confirmExpiry(pp);
110            }
111        }
112    }
113
114    /**
115     * Gets the account last joined - first joined (or Long.MAX_VALUE)
116     *
117     * @param uuid player uuid
118     * @return result
119     */
120    public long getAccountAge(UUID uuid) {
121        Long value = this.account_age_cache.get(uuid);
122        return value == null ? Long.MAX_VALUE : value;
123    }
124
125    public long getTimestamp(UUID uuid) {
126        Long value = this.dates_cache.get(uuid);
127        return value == null ? 0 : value;
128    }
129
130    public void updateExpired(Plot plot) {
131        if (plotsToDelete != null && !plotsToDelete.isEmpty() && plotsToDelete.contains(plot)) {
132            if (isExpired(new ArrayDeque<>(tasks), plot).isEmpty()) {
133                plotsToDelete.remove(plot);
134            }
135        }
136    }
137
138    public void confirmExpiry(final PlotPlayer<?> pp) {
139        TaskManager.runTask(() -> {
140            try (final MetaDataAccess<Boolean> metaDataAccess = pp.accessTemporaryMetaData(
141                    PlayerMetaDataKeys.TEMPORARY_IGNORE_EXPIRE_TASK)) {
142                if (metaDataAccess.isPresent()) {
143                    return;
144                }
145                if (plotsToDelete != null && !plotsToDelete.isEmpty() && pp.hasPermission("plots.admin.command.autoclear")) {
146                    final int num = plotsToDelete.size();
147                    while (!plotsToDelete.isEmpty()) {
148                        Iterator<Plot> iter = plotsToDelete.iterator();
149                        final Plot current = iter.next();
150                        if (!isExpired(new ArrayDeque<>(tasks), current).isEmpty()) {
151                            metaDataAccess.set(true);
152                            current.getCenter(pp::teleport);
153                            metaDataAccess.remove();
154                            Caption msg = TranslatableCaption.of("expiry.expired_options_clicky");
155                            Template numTemplate = Template.of("num", String.valueOf(num));
156                            Template areIsTemplate = Template.of("are_or_is", (num > 1 ? "plots are" : "plot is"));
157                            Template list_cmd = Template.of("list_cmd", "/plot list expired");
158                            Template plot = Template.of("plot", current.toString());
159                            Template cmd_del = Template.of("cmd_del", "/plot delete");
160                            Template cmd_keep_1d = Template.of("cmd_keep_1d", "/plot flag set keep 1d");
161                            Template cmd_keep = Template.of("cmd_keep", "/plot flag set keep true");
162                            Template cmd_no_show_expir = Template.of("cmd_no_show_expir", "/plot toggle clear-confirmation");
163                            pp.sendMessage(
164                                    msg,
165                                    numTemplate,
166                                    areIsTemplate,
167                                    list_cmd,
168                                    plot,
169                                    cmd_del,
170                                    cmd_keep_1d,
171                                    cmd_keep,
172                                    cmd_no_show_expir
173                            );
174                            return;
175                        } else {
176                            iter.remove();
177                        }
178                    }
179                    plotsToDelete.clear();
180                }
181            }
182        });
183    }
184
185
186    public boolean cancelTask() {
187        if (this.running != 2) {
188            return false;
189        }
190        this.running = 1;
191        return true;
192    }
193
194    public boolean runAutomatedTask() {
195        return runTask(new RunnableVal3<>() {
196            @Override
197            public void run(Plot plot, Runnable runnable, Boolean confirm) {
198                if (confirm) {
199                    if (plotsToDelete == null) {
200                        plotsToDelete = new HashSet<>();
201                    }
202                    plotsToDelete.add(plot);
203                    runnable.run();
204                } else {
205                    deleteWithMessage(plot, runnable);
206                }
207            }
208        });
209    }
210
211    public Collection<ExpiryTask> isExpired(ArrayDeque<ExpiryTask> applicable, Plot plot) {
212        // Filter out invalid worlds
213        for (int i = 0; i < applicable.size(); i++) {
214            ExpiryTask et = applicable.poll();
215            if (et.applies(plot.getArea())) {
216                applicable.add(et);
217            }
218        }
219        if (applicable.isEmpty()) {
220            return new ArrayList<>();
221        }
222
223        // Don't delete server plots
224        if (plot.getFlag(ServerPlotFlag.class)) {
225            return new ArrayList<>();
226        }
227
228        // Filter out non old plots
229        boolean shouldCheckAccountAge = false;
230        for (int i = 0; i < applicable.size(); i++) {
231            ExpiryTask et = applicable.poll();
232            if (et.applies(getAge(plot, et.shouldDeleteForUnknownOwner()))) {
233                applicable.add(et);
234                shouldCheckAccountAge |= et.getSettings().SKIP_ACCOUNT_AGE_DAYS != -1;
235            }
236        }
237        if (applicable.isEmpty()) {
238            return new ArrayList<>();
239        }
240        // Check account age
241        if (shouldCheckAccountAge) {
242            for (int i = 0; i < applicable.size(); i++) {
243                ExpiryTask et = applicable.poll();
244                long accountAge = getAge(plot, et.shouldDeleteForUnknownOwner());
245                if (et.appliesAccountAge(accountAge)) {
246                    applicable.add(et);
247                }
248            }
249            if (applicable.isEmpty()) {
250                return new ArrayList<>();
251            }
252        }
253
254        // Run applicable non confirming tasks
255        for (int i = 0; i < applicable.size(); i++) {
256            ExpiryTask expiryTask = applicable.poll();
257            if (!expiryTask.needsAnalysis() || plot.getArea().getType() != PlotAreaType.NORMAL) {
258                if (!expiryTask.requiresConfirmation()) {
259                    return Collections.singletonList(expiryTask);
260                }
261            }
262            applicable.add(expiryTask);
263        }
264        // Run applicable confirming tasks
265        for (int i = 0; i < applicable.size(); i++) {
266            ExpiryTask expiryTask = applicable.poll();
267            if (!expiryTask.needsAnalysis() || plot.getArea().getType() != PlotAreaType.NORMAL) {
268                return Collections.singletonList(expiryTask);
269            }
270            applicable.add(expiryTask);
271        }
272        return applicable;
273    }
274
275    public ArrayDeque<ExpiryTask> getTasks(PlotArea area) {
276        ArrayDeque<ExpiryTask> queue = new ArrayDeque<>(tasks);
277        queue.removeIf(expiryTask -> !expiryTask.applies(area));
278        return queue;
279    }
280
281    public void passesComplexity(
282            PlotAnalysis analysis, Collection<ExpiryTask> applicable,
283            RunnableVal<Boolean> success, Runnable failure
284    ) {
285        if (analysis != null) {
286            // Run non confirming tasks
287            for (ExpiryTask et : applicable) {
288                if (!et.requiresConfirmation() && et.applies(analysis)) {
289                    success.run(false);
290                    return;
291                }
292            }
293            for (ExpiryTask et : applicable) {
294                if (et.applies(analysis)) {
295                    success.run(true);
296                    return;
297                }
298            }
299            failure.run();
300        }
301    }
302
303    public boolean runTask(final RunnableVal3<Plot, Runnable, Boolean> expiredTask) {
304        if (this.running != 0) {
305            return false;
306        }
307        this.running = 2;
308        TaskManager.runTaskAsync(new Runnable() {
309            private ConcurrentLinkedDeque<Plot> plots = null;
310
311            @Override
312            public void run() {
313                final Runnable task = this;
314                if (ExpireManager.this.running != 2) {
315                    ExpireManager.this.running = 0;
316                    return;
317                }
318                if (plots == null) {
319                    plots = new ConcurrentLinkedDeque<>(PlotQuery.newQuery().allPlots().asList());
320                }
321                while (!plots.isEmpty()) {
322                    if (ExpireManager.this.running != 2) {
323                        ExpireManager.this.running = 0;
324                        return;
325                    }
326                    Plot plot = plots.poll();
327                    PlotArea area = plot.getArea();
328                    final Plot newPlot = area.getPlot(plot.getId());
329                    final ArrayDeque<ExpiryTask> applicable = new ArrayDeque<>(tasks);
330                    final Collection<ExpiryTask> expired = isExpired(applicable, newPlot);
331                    if (expired.isEmpty()) {
332                        continue;
333                    }
334                    for (ExpiryTask expiryTask : expired) {
335                        if (!expiryTask.needsAnalysis()) {
336                            expiredTask.run(newPlot, () -> TaskManager.getPlatformImplementation()
337                                            .taskLaterAsync(task, TaskTime.ticks(1L)),
338                                    expiryTask.requiresConfirmation()
339                            );
340                            return;
341                        }
342                    }
343                    final RunnableVal<PlotAnalysis> handleAnalysis =
344                            new RunnableVal<>() {
345                                @Override
346                                public void run(final PlotAnalysis changed) {
347                                    passesComplexity(changed, expired, new RunnableVal<>() {
348                                        @Override
349                                        public void run(Boolean confirmation) {
350                                            expiredTask.run(
351                                                    newPlot,
352                                                    () -> TaskManager
353                                                            .getPlatformImplementation()
354                                                            .taskLaterAsync(task, TaskTime.ticks(1L)),
355                                                    confirmation
356                                            );
357                                        }
358                                    }, () -> {
359                                        PlotFlag<?, ?> plotFlag = GlobalFlagContainer.getInstance()
360                                                .getFlag(AnalysisFlag.class)
361                                                .createFlagInstance(changed.asList());
362                                        PlotFlagAddEvent event =
363                                                eventDispatcher.callFlagAdd(plotFlag, plot);
364                                        if (event.getEventResult() == Result.DENY) {
365                                            return;
366                                        }
367                                        newPlot.setFlag(event.getFlag());
368                                        TaskManager.runTaskLaterAsync(task, TaskTime.seconds(1L));
369                                    });
370                                }
371                            };
372                    final Runnable doAnalysis =
373                            () -> PlotSquared.platform().hybridUtils().analyzePlot(newPlot, handleAnalysis);
374
375                    PlotAnalysis analysis = newPlot.getComplexity(null);
376                    if (analysis != null) {
377                        passesComplexity(analysis, expired, new RunnableVal<>() {
378                            @Override
379                            public void run(Boolean value) {
380                                doAnalysis.run();
381                            }
382                        }, () -> TaskManager.getPlatformImplementation().taskLaterAsync(task, TaskTime.ticks(1L)));
383                    } else {
384                        doAnalysis.run();
385                    }
386                    return;
387                }
388                if (plots.isEmpty()) {
389                    ExpireManager.this.running = 3;
390                    TaskManager.runTaskLater(() -> {
391                        if (ExpireManager.this.running == 3) {
392                            ExpireManager.this.running = 2;
393                            runTask(expiredTask);
394                        }
395                    }, TaskTime.ticks(86400000L));
396                } else {
397                    TaskManager.runTaskLaterAsync(task, TaskTime.seconds(10L));
398                }
399            }
400        });
401        return true;
402    }
403
404    public void storeDate(UUID uuid, long time) {
405        Long existing = this.dates_cache.put(uuid, time);
406        if (existing != null) {
407            long diff = time - existing;
408            if (diff > 0) {
409                Long account_age = this.account_age_cache.get(uuid);
410                if (account_age != null) {
411                    this.account_age_cache.put(uuid, account_age + diff);
412                }
413            }
414        }
415    }
416
417    public HashSet<Plot> getPendingExpired() {
418        return plotsToDelete == null ? new HashSet<>() : plotsToDelete;
419    }
420
421    public void deleteWithMessage(Plot plot, Runnable whenDone) {
422        if (plot.isMerged()) {
423            PlotUnlinkEvent event = this.eventDispatcher
424                    .callUnlink(plot.getArea(), plot, true, false,
425                            PlotUnlinkEvent.REASON.EXPIRE_DELETE
426                    );
427            if (event.getEventResult() != Result.DENY && plot.getPlotModificationManager().unlinkPlot(
428                    event.isCreateRoad(),
429                    event.isCreateSign()
430            )) {
431                this.eventDispatcher.callPostUnlink(plot, PlotUnlinkEvent.REASON.EXPIRE_DELETE);
432            }
433        }
434        for (UUID helper : plot.getTrusted()) {
435            PlotPlayer<?> player = PlotSquared.platform().playerManager().getPlayerIfExists(helper);
436            if (player != null) {
437                player.sendMessage(
438                        TranslatableCaption.of("trusted.plot_removed_user"),
439                        Templates.of("plot", plot.toString())
440                );
441            }
442        }
443        for (UUID helper : plot.getMembers()) {
444            PlotPlayer<?> player = PlotSquared.platform().playerManager().getPlayerIfExists(helper);
445            if (player != null) {
446                player.sendMessage(
447                        TranslatableCaption.of("trusted.plot_removed_user"),
448                        Templates.of("plot", plot.toString())
449                );
450            }
451        }
452        plot.getPlotModificationManager().deletePlot(null, whenDone);
453    }
454
455    @Deprecated(forRemoval = true, since = "6.4.0")
456    public long getAge(UUID uuid) {
457        return getAge(uuid, false);
458    }
459
460    /**
461     * Get the age (last play time) of the passed player
462     *
463     * @param uuid                     the uuid of the owner to check against
464     * @param shouldDeleteUnknownOwner {@code true} if an unknown player should be counted as never online
465     * @return the millis since the player was last online, or {@link Long#MAX_VALUE} if player was never online
466     * @since 6.4.0
467     */
468    public long getAge(UUID uuid, final boolean shouldDeleteUnknownOwner) {
469        if (PlotSquared.platform().playerManager().getPlayerIfExists(uuid) != null) {
470            return 0;
471        }
472        Long last = this.dates_cache.get(uuid);
473        if (last == null) {
474            OfflinePlotPlayer opp = PlotSquared.platform().playerManager().getOfflinePlayer(uuid);
475            if (opp != null && (last = opp.getLastPlayed()) != 0) {
476                this.dates_cache.put(uuid, last);
477            } else {
478                return shouldDeleteUnknownOwner ? Long.MAX_VALUE : 0;
479            }
480        }
481        if (last == 0) {
482            return 0;
483        }
484        return System.currentTimeMillis() - last;
485    }
486
487    public long getAge(Plot plot, final boolean shouldDeleteUnknownOwner) {
488        if (!plot.hasOwner() || Objects.equals(DBFunc.EVERYONE, plot.getOwner())
489                || PlotSquared.platform().playerManager().getPlayerIfExists(plot.getOwner()) != null || plot.getRunning() > 0) {
490            return 0;
491        }
492
493        final Object value = plot.getFlag(KeepFlag.class);
494        if (!value.equals(false)) {
495            if (value instanceof Boolean) {
496                if (Boolean.TRUE.equals(value)) {
497                    return 0;
498                }
499            } else if (value instanceof Long) {
500                if ((Long) value > System.currentTimeMillis()) {
501                    return 0;
502                }
503            } else { // Invalid?
504                return 0;
505            }
506        }
507        long min = Long.MAX_VALUE;
508        for (UUID owner : plot.getOwners()) {
509            long age = getAge(owner, shouldDeleteUnknownOwner);
510            if (age < min) {
511                min = age;
512            }
513        }
514        return min;
515    }
516
517}