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