Add basic statistics to Abilities and Replies
This commit is contained in:
parent
f84ec0b020
commit
9ffc547cdf
@ -31,4 +31,7 @@ As an example, if you want to restrict the updates to photos only, then you may
|
||||
public boolean checkGlobalFlags(Update update) {
|
||||
return Flag.PHOTO;
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
## Statistics
|
||||
AbilityBot can accrue basic statistics about the usage of your abilities and replies. Simply `enableStats()` on an Ability builder or `enableStats(<name>)` on replies to activate this feature. Once activated, you may call `/stats` and the bot will print a basic list of statistics. At the moment, AbilityBot only tracks hits. In the future, this will be enhanced to track more stats.
|
@ -1,6 +1,6 @@
|
||||
package org.telegram.abilitybots.api.bot;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.*;
|
||||
import com.google.common.collect.ImmutableList.Builder;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@ -33,15 +33,18 @@ import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static com.google.common.collect.Sets.difference;
|
||||
import static java.lang.String.format;
|
||||
import static java.time.ZonedDateTime.now;
|
||||
import static java.util.Arrays.stream;
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static java.util.regex.Pattern.CASE_INSENSITIVE;
|
||||
import static java.util.regex.Pattern.compile;
|
||||
import static java.util.stream.Collectors.toSet;
|
||||
import static org.telegram.abilitybots.api.objects.Locality.*;
|
||||
import static org.telegram.abilitybots.api.objects.MessageContext.newContext;
|
||||
import static org.telegram.abilitybots.api.objects.Privacy.*;
|
||||
import static org.telegram.abilitybots.api.objects.Stats.createStats;
|
||||
import static org.telegram.abilitybots.api.util.AbilityMessageCodes.*;
|
||||
import static org.telegram.abilitybots.api.util.AbilityUtils.*;
|
||||
|
||||
@ -87,6 +90,7 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability
|
||||
public static final String USERS = "USERS";
|
||||
public static final String USER_ID = "USER_ID";
|
||||
public static final String BLACKLIST = "BLACKLIST";
|
||||
public static final String STATS = "ABILITYBOT_STATS";
|
||||
|
||||
// DB and sender
|
||||
protected final DBContext db;
|
||||
@ -102,6 +106,7 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability
|
||||
|
||||
// Ability registry
|
||||
private Map<String, Ability> abilities;
|
||||
private Map<String, Stats> stats;
|
||||
|
||||
// Reply registry
|
||||
private List<Reply> replies;
|
||||
@ -119,6 +124,7 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability
|
||||
silent = new SilentSender(sender);
|
||||
|
||||
registerAbilities();
|
||||
initStats();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -149,6 +155,13 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability
|
||||
return db.getSet(ADMINS);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a mapping of ability and reply names to their corresponding statistics
|
||||
*/
|
||||
protected Map<String, Stats> stats() {
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the immutable map of <String,Ability>
|
||||
*/
|
||||
@ -163,6 +176,7 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability
|
||||
return replies;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This method contains the stream of actions that are applied on any update.
|
||||
* <p>
|
||||
@ -188,6 +202,7 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability
|
||||
.filter(this::checkMessageFlags)
|
||||
.map(this::getContext)
|
||||
.map(this::consumeUpdate)
|
||||
.map(this::updateStats)
|
||||
.forEach(this::postConsumption);
|
||||
|
||||
// Commit to DB now after all the actions have been dealt
|
||||
@ -275,6 +290,19 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability
|
||||
}
|
||||
}
|
||||
|
||||
private void initStats() {
|
||||
Set<String> enabledStats = Stream.concat(
|
||||
replies.stream().filter(Reply::statsEnabled).map(Reply::name),
|
||||
abilities.entrySet().stream()
|
||||
.filter(entry -> entry.getValue().statsEnabled())
|
||||
.map(Map.Entry::getKey)).collect(toSet());
|
||||
stats = db.getMap(STATS);
|
||||
Set<String> toBeRemoved = difference(stats.keySet(), enabledStats);
|
||||
toBeRemoved.forEach(stats::remove);
|
||||
enabledStats.forEach(abName -> stats.computeIfAbsent(abName,
|
||||
name -> createStats(abName, 0)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param clazz the type to be tested
|
||||
* @return a predicate testing the return type of the method corresponding to the class parameter
|
||||
@ -344,6 +372,26 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability
|
||||
return pair;
|
||||
}
|
||||
|
||||
Pair<MessageContext, Ability> updateStats(Pair<MessageContext, Ability> pair) {
|
||||
Ability ab = pair.b();
|
||||
if (ab.statsEnabled()) {
|
||||
updateStats(pair.b().name());
|
||||
}
|
||||
return pair;
|
||||
}
|
||||
|
||||
private void updateReplyStats(Reply reply) {
|
||||
if (reply.statsEnabled()) {
|
||||
updateStats(reply.name());
|
||||
}
|
||||
}
|
||||
|
||||
void updateStats(String name) {
|
||||
Stats statsObj = stats.get(name);
|
||||
statsObj.hit();
|
||||
stats.put(name, statsObj);
|
||||
}
|
||||
|
||||
Pair<MessageContext, Ability> getContext(Trio<Update, Ability, String[]> trio) {
|
||||
Update update = trio.a();
|
||||
User user = AbilityUtils.getUser(update);
|
||||
@ -504,6 +552,7 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability
|
||||
.filter(reply -> reply.isOkFor(update))
|
||||
.map(reply -> {
|
||||
reply.actOn(update);
|
||||
updateReplyStats(reply);
|
||||
return false;
|
||||
})
|
||||
.reduce(true, Boolean::logicalAnd);
|
||||
|
@ -26,6 +26,7 @@ import java.io.PrintStream;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
import static com.google.common.base.Strings.isNullOrEmpty;
|
||||
import static com.google.common.collect.MultimapBuilder.hashKeys;
|
||||
@ -76,6 +77,7 @@ public final class DefaultAbilities implements AbilityExtension {
|
||||
public static final String RECOVER = "recover";
|
||||
public static final String COMMANDS = "commands";
|
||||
public static final String REPORT = "report";
|
||||
public static final String STATS = "stats";
|
||||
private static final Logger log = LoggerFactory.getLogger(DefaultAbilities.class);
|
||||
private final BaseAbilityBot bot;
|
||||
|
||||
@ -179,6 +181,26 @@ public final class DefaultAbilities implements AbilityExtension {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the ability to report statistics for abilities and replies.
|
||||
*/
|
||||
public Ability reportStats() {
|
||||
return builder()
|
||||
.name(STATS)
|
||||
.locality(ALL)
|
||||
.privacy(ADMIN)
|
||||
.input(0)
|
||||
.action(ctx -> {
|
||||
String stats = bot.stats().entrySet().stream()
|
||||
.map(entry -> String.format("%s: %d", entry.getKey(), entry.getValue().hits()))
|
||||
.reduce(new StringJoiner("\n"), StringJoiner::add, StringJoiner::merge)
|
||||
.toString();
|
||||
|
||||
bot.silent.send(stats, ctx.chatId());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* This backup ability returns the object defined by {@link DBContext#backup()} as a message document.
|
||||
* <p>
|
||||
@ -212,6 +234,7 @@ public final class DefaultAbilities implements AbilityExtension {
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Recovers the bot database using {@link DBContext#recover(Object)}.
|
||||
* <p>
|
||||
|
@ -42,13 +42,14 @@ public final class Ability {
|
||||
private final Locality locality;
|
||||
private final Privacy privacy;
|
||||
private final int argNum;
|
||||
private final boolean statsEnabled;
|
||||
private final Consumer<MessageContext> action;
|
||||
private final Consumer<MessageContext> postAction;
|
||||
private final List<Reply> replies;
|
||||
private final List<Predicate<Update>> flags;
|
||||
|
||||
@SafeVarargs
|
||||
private Ability(String name, String info, Locality locality, Privacy privacy, int argNum, Consumer<MessageContext> action, Consumer<MessageContext> postAction, List<Reply> replies, Predicate<Update>... flags) {
|
||||
private Ability(String name, String info, Locality locality, Privacy privacy, int argNum, boolean statsEnabled, Consumer<MessageContext> action, Consumer<MessageContext> postAction, List<Reply> replies, Predicate<Update>... flags) {
|
||||
checkArgument(!isEmpty(name), "Method name cannot be empty");
|
||||
checkArgument(!containsWhitespace(name), "Method name cannot contain spaces");
|
||||
checkArgument(isAlphanumeric(name), "Method name can only be alpha-numeric", name);
|
||||
@ -70,6 +71,7 @@ public final class Ability {
|
||||
|
||||
this.postAction = postAction;
|
||||
this.replies = replies;
|
||||
this.statsEnabled = statsEnabled;
|
||||
}
|
||||
|
||||
public static AbilityBuilder builder() {
|
||||
@ -96,6 +98,10 @@ public final class Ability {
|
||||
return argNum;
|
||||
}
|
||||
|
||||
public boolean statsEnabled() {
|
||||
return statsEnabled;
|
||||
}
|
||||
|
||||
public Consumer<MessageContext> action() {
|
||||
return action;
|
||||
}
|
||||
@ -147,12 +153,14 @@ public final class Ability {
|
||||
private Privacy privacy;
|
||||
private Locality locality;
|
||||
private int argNum;
|
||||
private boolean statsEnabled;
|
||||
private Consumer<MessageContext> action;
|
||||
private Consumer<MessageContext> postAction;
|
||||
private List<Reply> replies;
|
||||
private Predicate<Update>[] flags;
|
||||
|
||||
private AbilityBuilder() {
|
||||
statsEnabled = false;
|
||||
replies = newArrayList();
|
||||
}
|
||||
|
||||
@ -186,6 +194,11 @@ public final class Ability {
|
||||
return this;
|
||||
}
|
||||
|
||||
public AbilityBuilder enableStats() {
|
||||
statsEnabled = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AbilityBuilder privacy(Privacy privacy) {
|
||||
this.privacy = privacy;
|
||||
return this;
|
||||
@ -202,6 +215,11 @@ public final class Ability {
|
||||
return this;
|
||||
}
|
||||
|
||||
public final AbilityBuilder reply(Reply reply) {
|
||||
replies.add(reply);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AbilityBuilder basedOn(Ability ability) {
|
||||
replies.clear();
|
||||
replies.addAll(ability.replies());
|
||||
@ -216,7 +234,7 @@ public final class Ability {
|
||||
}
|
||||
|
||||
public Ability build() {
|
||||
return new Ability(name, info, locality, privacy, argNum, action, postAction, replies, flags);
|
||||
return new Ability(name, info, locality, privacy, argNum, statsEnabled, action, postAction, replies, flags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,12 +26,15 @@ import static java.util.Arrays.asList;
|
||||
public class Reply {
|
||||
public final List<Predicate<Update>> conditions;
|
||||
public final Consumer<Update> action;
|
||||
private boolean statsEnabled;
|
||||
private String name;
|
||||
|
||||
Reply(List<Predicate<Update>> conditions, Consumer<Update> action) {
|
||||
this.conditions = ImmutableList.<Predicate<Update>>builder()
|
||||
.addAll(conditions)
|
||||
.build();
|
||||
this.action = action;
|
||||
statsEnabled = false;
|
||||
}
|
||||
|
||||
public static Reply of(Consumer<Update> action, List<Predicate<Update>> conditions) {
|
||||
@ -65,6 +68,20 @@ public class Reply {
|
||||
return Stream.of(this);
|
||||
}
|
||||
|
||||
public Reply enableStats(String name) {
|
||||
this.name = name;
|
||||
statsEnabled = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean statsEnabled() {
|
||||
return statsEnabled;
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
|
@ -0,0 +1,64 @@
|
||||
package org.telegram.abilitybots.api.objects;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.base.MoreObjects;
|
||||
import org.json.JSONPropertyIgnore;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Basic POJO to track ability and reply hits. The current implementation is NOT thread safe.
|
||||
*
|
||||
* @author Abbas Abou Daya
|
||||
*/
|
||||
public final class Stats implements Serializable {
|
||||
@JsonProperty
|
||||
private final String name;
|
||||
@JsonProperty
|
||||
private long hits;
|
||||
|
||||
private Stats(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@JsonCreator
|
||||
public static Stats createStats(@JsonProperty("name") String name, @JsonProperty("hits") long hits) {
|
||||
return new Stats(name);
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public long hits() {
|
||||
return hits;
|
||||
}
|
||||
|
||||
public void hit() {
|
||||
hits++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Stats that = (Stats) o;
|
||||
return hits == that.hits &&
|
||||
Objects.equals(name, that.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(name, hits);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("name", name)
|
||||
.add("hits", hits)
|
||||
.toString();
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
import org.mockito.Mockito;
|
||||
import org.telegram.abilitybots.api.db.DBContext;
|
||||
import org.telegram.abilitybots.api.objects.*;
|
||||
import org.telegram.abilitybots.api.sender.MessageSender;
|
||||
@ -130,6 +131,30 @@ public class AbilityBotTest {
|
||||
verify(sender, times(1)).sendDocument(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void canReportStatistics() {
|
||||
MessageContext context = defaultContext();
|
||||
|
||||
defaultAbs.reportStats().action().accept(context);
|
||||
|
||||
verify(silent, times(1)).send("count: 0\nmustreply: 0", GROUP_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
void canReportUpdatedStatistics() {
|
||||
Update upd1 = mockFullUpdate(bot, CREATOR, "/count 1 2 3 4");
|
||||
bot.onUpdateReceived(upd1);
|
||||
Update upd2 = mockFullUpdate(bot, CREATOR, "must reply");
|
||||
bot.onUpdateReceived(upd2);
|
||||
|
||||
Mockito.reset(silent);
|
||||
|
||||
Update statUpd = mockFullUpdate(bot, CREATOR, "/stats");
|
||||
bot.onUpdateReceived(statUpd);
|
||||
|
||||
verify(silent, times(1)).send("count: 1\nmustreply: 1", CREATOR.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void canRecoverDB() throws TelegramApiException, IOException {
|
||||
Update update = mockBackupUpdate();
|
||||
@ -553,7 +578,7 @@ public class AbilityBotTest {
|
||||
|
||||
defaultAbs.commands().action().accept(creatorCtx);
|
||||
|
||||
String expected = "PUBLIC\n/commands\n/count\n/default - dis iz default command\n/group\n/test\nADMIN\n/admin\n/ban\n/demote\n/promote\n/unban\nCREATOR\n/backup\n/claim\n/recover\n/report";
|
||||
String expected = "PUBLIC\n/commands\n/count\n/default - dis iz default command\n/group\n/test\nADMIN\n/admin\n/ban\n/demote\n/promote\n/stats\n/unban\nCREATOR\n/backup\n/claim\n/recover\n/report";
|
||||
verify(silent, times(1)).send(expected, GROUP_ID);
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@ package org.telegram.abilitybots.api.bot;
|
||||
import org.telegram.abilitybots.api.db.DBContext;
|
||||
import org.telegram.abilitybots.api.objects.Ability;
|
||||
import org.telegram.abilitybots.api.objects.Ability.AbilityBuilder;
|
||||
import org.telegram.abilitybots.api.objects.Reply;
|
||||
import org.telegram.abilitybots.api.toggle.AbilityToggle;
|
||||
|
||||
import static org.telegram.abilitybots.api.objects.Ability.builder;
|
||||
@ -41,7 +42,7 @@ public class DefaultBot extends AbilityBot {
|
||||
return getDefaultBuilder()
|
||||
.name(DEFAULT)
|
||||
.info("dis iz default command")
|
||||
.reply(upd -> silent.send("reply", upd.getMessage().getChatId()), MESSAGE, update -> update.getMessage().getText().equals("must reply"))
|
||||
.reply(Reply.of(upd -> silent.send("reply", upd.getMessage().getChatId()), MESSAGE, update -> update.getMessage().getText().equals("must reply")).enableStats("mustreply"))
|
||||
.reply(upd -> silent.send("reply", upd.getCallbackQuery().getMessage().getChatId()), CALLBACK_QUERY)
|
||||
.build();
|
||||
}
|
||||
@ -67,6 +68,7 @@ public class DefaultBot extends AbilityBot {
|
||||
.privacy(PUBLIC)
|
||||
.locality(USER)
|
||||
.input(4)
|
||||
.enableStats()
|
||||
.build();
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user