From 4aa753515ddf7dd1b29d984b997e1d29fc85a5d9 Mon Sep 17 00:00:00 2001
From: Luck <git@lucko.me>
Date: Sat, 20 Aug 2016 15:43:54 +0100
Subject: [PATCH] Add events

---
 README.md                                     |  37 ++++-
 api/pom.xml                                   |  26 ++--
 .../me/lucko/luckperms/api/LuckPermsApi.java  |  14 ++
 .../api/event/AbstractPermissionAddEvent.java |  57 +++++++
 .../event/AbstractPermissionRemoveEvent.java  |  54 +++++++
 .../luckperms/api/event/CancellableEvent.java |  40 +++++
 .../me/lucko/luckperms/api/event/LPEvent.java |  58 +++++++
 .../lucko/luckperms/api/event/LPListener.java |  29 ++++
 .../luckperms/api/event/TargetedEvent.java    |  37 +++++
 .../lucko/luckperms/api/event/TrackEvent.java |  58 +++++++
 .../lucko/luckperms/api/event/UserEvent.java  |  39 +++++
 .../api/event/events/GroupAddEvent.java       |  41 +++++
 .../api/event/events/GroupRemoveEvent.java    |  40 +++++
 .../api/event/events/LogNotifyEvent.java      |  41 +++++
 .../event/events/PermissionExpireEvent.java   |  40 +++++
 .../api/event/events/PermissionSetEvent.java  |  53 +++++++
 .../event/events/PermissionUnsetEvent.java    |  40 +++++
 .../api/event/events/PostSyncEvent.java       |  33 ++++
 .../api/event/events/PreSyncEvent.java        |  33 ++++
 .../api/event/events/UserDemoteEvent.java     |  35 +++++
 .../api/event/events/UserFirstLoginEvent.java |  51 +++++++
 .../events/UserPermissionRefreshEvent.java    |  34 +++++
 .../api/event/events/UserPromoteEvent.java    |  35 +++++
 bukkit/pom.xml                                |   2 +-
 .../me/lucko/luckperms/LPBukkitPlugin.java    |   7 +-
 .../me/lucko/luckperms/users/BukkitUser.java  |   4 +
 bungee/pom.xml                                |   2 +-
 .../me/lucko/luckperms/LPBungeePlugin.java    |   4 +-
 common/pom.xml                                |   2 +-
 .../me/lucko/luckperms/LuckPermsPlugin.java   |  49 +-----
 .../api/implementation/ApiProvider.java       |  28 ++++
 .../internal/PermissionHolderLink.java        |   5 +-
 .../commands/user/subcommands/UserDemote.java |   4 +
 .../user/subcommands/UserPromote.java         |   4 +
 .../luckperms/core/PermissionHolder.java      | 141 +++++++++++++-----
 .../me/lucko/luckperms/data/LogEntry.java     |   5 +
 .../java/me/lucko/luckperms/groups/Group.java |  15 +-
 .../lucko/luckperms/runnables/UpdateTask.java |  11 ++
 .../java/me/lucko/luckperms/users/User.java   |  11 +-
 .../luckperms/utils/AbstractListener.java     |   7 +
 pom.xml                                       |   4 +-
 sponge/pom.xml                                |   2 +-
 .../me/lucko/luckperms/LPSpongePlugin.java    |   7 +-
 43 files changed, 1128 insertions(+), 111 deletions(-)
 create mode 100644 api/src/main/java/me/lucko/luckperms/api/event/AbstractPermissionAddEvent.java
 create mode 100644 api/src/main/java/me/lucko/luckperms/api/event/AbstractPermissionRemoveEvent.java
 create mode 100644 api/src/main/java/me/lucko/luckperms/api/event/CancellableEvent.java
 create mode 100644 api/src/main/java/me/lucko/luckperms/api/event/LPEvent.java
 create mode 100644 api/src/main/java/me/lucko/luckperms/api/event/LPListener.java
 create mode 100644 api/src/main/java/me/lucko/luckperms/api/event/TargetedEvent.java
 create mode 100644 api/src/main/java/me/lucko/luckperms/api/event/TrackEvent.java
 create mode 100644 api/src/main/java/me/lucko/luckperms/api/event/UserEvent.java
 create mode 100644 api/src/main/java/me/lucko/luckperms/api/event/events/GroupAddEvent.java
 create mode 100644 api/src/main/java/me/lucko/luckperms/api/event/events/GroupRemoveEvent.java
 create mode 100644 api/src/main/java/me/lucko/luckperms/api/event/events/LogNotifyEvent.java
 create mode 100644 api/src/main/java/me/lucko/luckperms/api/event/events/PermissionExpireEvent.java
 create mode 100644 api/src/main/java/me/lucko/luckperms/api/event/events/PermissionSetEvent.java
 create mode 100644 api/src/main/java/me/lucko/luckperms/api/event/events/PermissionUnsetEvent.java
 create mode 100644 api/src/main/java/me/lucko/luckperms/api/event/events/PostSyncEvent.java
 create mode 100644 api/src/main/java/me/lucko/luckperms/api/event/events/PreSyncEvent.java
 create mode 100644 api/src/main/java/me/lucko/luckperms/api/event/events/UserDemoteEvent.java
 create mode 100644 api/src/main/java/me/lucko/luckperms/api/event/events/UserFirstLoginEvent.java
 create mode 100644 api/src/main/java/me/lucko/luckperms/api/event/events/UserPermissionRefreshEvent.java
 create mode 100644 api/src/main/java/me/lucko/luckperms/api/event/events/UserPromoteEvent.java

diff --git a/README.md b/README.md
index ee74d7e9..63c63323 100644
--- a/README.md
+++ b/README.md
@@ -152,11 +152,46 @@ You can add LuckPerms as a Maven dependency by adding the following to your proj
     <dependency>
         <groupId>me.lucko.luckperms</groupId>
         <artifactId>luckperms-api</artifactId>
-        <version>2.4</version>
+        <version>2.5</version>
     </dependency>
 </dependencies>
 ````
 
+### Events
+LuckPerms exposes a full read/write API, as well as an event listening system. Due to the multi-platform nature of the project, an internal Event system is used, as opposed to the systems already in place on each platform. (the Bukkit Event api, for example). This means that simply registering your listener with the platform is not sufficient.
+
+All events are *fired asynchronously*. This means you should not interact with or call any non-thread safe methods from within listeners.
+
+To listen to an event, you need to first make a class that implements `LPListener`. Then within this class, you can define all of your listener methods.
+
+Each listener method must be annotated with `@Subscribe`. For example...
+
+```java
+package me.lucko.test;
+
+import com.google.common.eventbus.Subscribe;
+import me.lucko.luckperms.api.event.LPListener;
+import me.lucko.luckperms.api.event.events.PermissionSetEvent;
+
+public class TestListener implements LPListener {
+
+    @Subscribe
+    public void onPermissionSet(PermissionSetEvent event) {
+
+    }
+
+}
+```
+
+You also need to register your new Listener with the API.
+```java
+@Override
+public void onEnable() {
+    LuckPermsApi api;
+    api.registerListener(new TestListener());
+}
+```
+
 ## Versioning
 As of version 2.0, LuckPerms roughly follows the standards set out in Semantic Versioning.
 
diff --git a/api/pom.xml b/api/pom.xml
index 77babf55..a44da7cd 100644
--- a/api/pom.xml
+++ b/api/pom.xml
@@ -5,7 +5,7 @@
     <parent>
         <artifactId>luckperms</artifactId>
         <groupId>me.lucko.luckperms</groupId>
-        <version>2.4-SNAPSHOT</version>
+        <version>2.5-SNAPSHOT</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
@@ -21,19 +21,17 @@
                     <target>1.8</target>
                 </configuration>
             </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-source-plugin</artifactId>
-                <version>3.0.1</version>
-                <executions>
-                    <execution>
-                        <id>attach-sources</id>
-                        <goals>
-                            <goal>jar</goal>
-                        </goals>
-                    </execution>
-                </executions>
-            </plugin>
         </plugins>
     </build>
+
+    <dependencies>
+        <!-- guava -->
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>19.0</version>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
 </project>
diff --git a/api/src/main/java/me/lucko/luckperms/api/LuckPermsApi.java b/api/src/main/java/me/lucko/luckperms/api/LuckPermsApi.java
index 043ac566..af251ca1 100644
--- a/api/src/main/java/me/lucko/luckperms/api/LuckPermsApi.java
+++ b/api/src/main/java/me/lucko/luckperms/api/LuckPermsApi.java
@@ -22,6 +22,8 @@
 
 package me.lucko.luckperms.api;
 
+import me.lucko.luckperms.api.event.LPListener;
+
 import java.util.Optional;
 import java.util.Set;
 import java.util.UUID;
@@ -42,6 +44,18 @@ public interface LuckPermsApi {
      */
     String getVersion();
 
+    /**
+     * Registers a listener to be sent LuckPerms events
+     * @param listener the listener instance
+     */
+    void registerListener(LPListener listener);
+
+    /**
+     * Unregisters a previously registered listener from the EventBus
+     * @param listener the listener instance to unregister
+     */
+    void unregisterListener(LPListener listener);
+
     /**
      * Gets a wrapped {@link LPConfiguration} instance, with read only access
      * @return a configuration instance
diff --git a/api/src/main/java/me/lucko/luckperms/api/event/AbstractPermissionAddEvent.java b/api/src/main/java/me/lucko/luckperms/api/event/AbstractPermissionAddEvent.java
new file mode 100644
index 00000000..3984df49
--- /dev/null
+++ b/api/src/main/java/me/lucko/luckperms/api/event/AbstractPermissionAddEvent.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2016 Lucko (Luck) <luck@lucko.me>
+ *
+ *  Permission is hereby granted, free of charge, to any person obtaining a copy
+ *  of this software and associated documentation files (the "Software"), to deal
+ *  in the Software without restriction, including without limitation the rights
+ *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ *  copies of the Software, and to permit persons to whom the Software is
+ *  furnished to do so, subject to the following conditions:
+ *
+ *  The above copyright notice and this permission notice shall be included in all
+ *  copies or substantial portions of the Software.
+ *
+ *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ *  SOFTWARE.
+ */
+
+package me.lucko.luckperms.api.event;
+
+import me.lucko.luckperms.api.PermissionHolder;
+
+import java.util.Optional;
+
+public class AbstractPermissionAddEvent extends TargetedEvent<PermissionHolder> {
+
+    private final String server;
+    private final String world;
+    private final long expiry;
+
+    protected AbstractPermissionAddEvent(String eventName, PermissionHolder target, String server, String world, long expiry) {
+        super(eventName, target);
+        this.server = server;
+        this.world = world;
+        this.expiry = expiry;
+    }
+
+    public Optional<String> getServer() {
+        return Optional.ofNullable(server);
+    }
+
+    public Optional<String> getWorld() {
+        return Optional.ofNullable(world);
+    }
+
+    public boolean isTemporary() {
+        return expiry != 0L;
+    }
+
+    public long getExpiry() {
+        return expiry;
+    }
+}
diff --git a/api/src/main/java/me/lucko/luckperms/api/event/AbstractPermissionRemoveEvent.java b/api/src/main/java/me/lucko/luckperms/api/event/AbstractPermissionRemoveEvent.java
new file mode 100644
index 00000000..a9304008
--- /dev/null
+++ b/api/src/main/java/me/lucko/luckperms/api/event/AbstractPermissionRemoveEvent.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2016 Lucko (Luck) <luck@lucko.me>
+ *
+ *  Permission is hereby granted, free of charge, to any person obtaining a copy
+ *  of this software and associated documentation files (the "Software"), to deal
+ *  in the Software without restriction, including without limitation the rights
+ *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ *  copies of the Software, and to permit persons to whom the Software is
+ *  furnished to do so, subject to the following conditions:
+ *
+ *  The above copyright notice and this permission notice shall be included in all
+ *  copies or substantial portions of the Software.
+ *
+ *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ *  SOFTWARE.
+ */
+
+package me.lucko.luckperms.api.event;
+
+import me.lucko.luckperms.api.PermissionHolder;
+
+import java.util.Optional;
+
+public class AbstractPermissionRemoveEvent extends TargetedEvent<PermissionHolder> {
+
+    private final String server;
+    private final String world;
+    private final boolean temporary;
+
+    protected AbstractPermissionRemoveEvent(String eventName, PermissionHolder target, String server, String world, boolean temporary) {
+        super(eventName, target);
+        this.server = server;
+        this.world = world;
+        this.temporary = temporary;
+    }
+
+    public Optional<String> getServer() {
+        return Optional.ofNullable(server);
+    }
+
+    public Optional<String> getWorld() {
+        return Optional.ofNullable(world);
+    }
+
+    public boolean getTemporary() {
+        return temporary;
+    }
+
+}
diff --git a/api/src/main/java/me/lucko/luckperms/api/event/CancellableEvent.java b/api/src/main/java/me/lucko/luckperms/api/event/CancellableEvent.java
new file mode 100644
index 00000000..3e198d28
--- /dev/null
+++ b/api/src/main/java/me/lucko/luckperms/api/event/CancellableEvent.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2016 Lucko (Luck) <luck@lucko.me>
+ *
+ *  Permission is hereby granted, free of charge, to any person obtaining a copy
+ *  of this software and associated documentation files (the "Software"), to deal
+ *  in the Software without restriction, including without limitation the rights
+ *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ *  copies of the Software, and to permit persons to whom the Software is
+ *  furnished to do so, subject to the following conditions:
+ *
+ *  The above copyright notice and this permission notice shall be included in all
+ *  copies or substantial portions of the Software.
+ *
+ *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ *  SOFTWARE.
+ */
+
+package me.lucko.luckperms.api.event;
+
+public class CancellableEvent extends LPEvent {
+
+    private boolean cancelled = false;
+
+    protected CancellableEvent(String eventName) {
+        super(eventName);
+    }
+
+    public boolean isCancelled() {
+        return cancelled;
+    }
+
+    public void setCancelled(boolean cancelled) {
+        this.cancelled = cancelled;
+    }
+}
diff --git a/api/src/main/java/me/lucko/luckperms/api/event/LPEvent.java b/api/src/main/java/me/lucko/luckperms/api/event/LPEvent.java
new file mode 100644
index 00000000..b0390c2c
--- /dev/null
+++ b/api/src/main/java/me/lucko/luckperms/api/event/LPEvent.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2016 Lucko (Luck) <luck@lucko.me>
+ *
+ *  Permission is hereby granted, free of charge, to any person obtaining a copy
+ *  of this software and associated documentation files (the "Software"), to deal
+ *  in the Software without restriction, including without limitation the rights
+ *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ *  copies of the Software, and to permit persons to whom the Software is
+ *  furnished to do so, subject to the following conditions:
+ *
+ *  The above copyright notice and this permission notice shall be included in all
+ *  copies or substantial portions of the Software.
+ *
+ *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ *  SOFTWARE.
+ */
+
+package me.lucko.luckperms.api.event;
+
+import me.lucko.luckperms.api.LuckPermsApi;
+
+public abstract class LPEvent {
+
+    /**
+     * A link to the API instance provided for convenience.
+     */
+    private LuckPermsApi api = null;
+
+    /**
+     * A friendly name of the event
+     */
+    private final String eventName;
+
+    protected LPEvent(String eventName) {
+        this.eventName = eventName;
+    }
+
+    public String getEventName() {
+        return eventName;
+    }
+
+    public LuckPermsApi getApi() {
+        return api;
+    }
+
+    public void setApi(LuckPermsApi api) {
+        if (this.api != null) {
+            throw new IllegalStateException("API can only be set once.");
+        }
+        this.api = api;
+    }
+
+}
diff --git a/api/src/main/java/me/lucko/luckperms/api/event/LPListener.java b/api/src/main/java/me/lucko/luckperms/api/event/LPListener.java
new file mode 100644
index 00000000..dc0bc789
--- /dev/null
+++ b/api/src/main/java/me/lucko/luckperms/api/event/LPListener.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2016 Lucko (Luck) <luck@lucko.me>
+ *
+ *  Permission is hereby granted, free of charge, to any person obtaining a copy
+ *  of this software and associated documentation files (the "Software"), to deal
+ *  in the Software without restriction, including without limitation the rights
+ *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ *  copies of the Software, and to permit persons to whom the Software is
+ *  furnished to do so, subject to the following conditions:
+ *
+ *  The above copyright notice and this permission notice shall be included in all
+ *  copies or substantial portions of the Software.
+ *
+ *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ *  SOFTWARE.
+ */
+
+package me.lucko.luckperms.api.event;
+
+/**
+ * Used to mark a class that listens for LuckPerms events
+ */
+public interface LPListener {
+}
diff --git a/api/src/main/java/me/lucko/luckperms/api/event/TargetedEvent.java b/api/src/main/java/me/lucko/luckperms/api/event/TargetedEvent.java
new file mode 100644
index 00000000..8328ec28
--- /dev/null
+++ b/api/src/main/java/me/lucko/luckperms/api/event/TargetedEvent.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2016 Lucko (Luck) <luck@lucko.me>
+ *
+ *  Permission is hereby granted, free of charge, to any person obtaining a copy
+ *  of this software and associated documentation files (the "Software"), to deal
+ *  in the Software without restriction, including without limitation the rights
+ *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ *  copies of the Software, and to permit persons to whom the Software is
+ *  furnished to do so, subject to the following conditions:
+ *
+ *  The above copyright notice and this permission notice shall be included in all
+ *  copies or substantial portions of the Software.
+ *
+ *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ *  SOFTWARE.
+ */
+
+package me.lucko.luckperms.api.event;
+
+public class TargetedEvent<T> extends LPEvent {
+
+    private final T target;
+
+    protected TargetedEvent(String eventName, T target) {
+        super(eventName);
+        this.target = target;
+    }
+
+    public T getTarget() {
+        return target;
+    }
+}
diff --git a/api/src/main/java/me/lucko/luckperms/api/event/TrackEvent.java b/api/src/main/java/me/lucko/luckperms/api/event/TrackEvent.java
new file mode 100644
index 00000000..234b1a73
--- /dev/null
+++ b/api/src/main/java/me/lucko/luckperms/api/event/TrackEvent.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2016 Lucko (Luck) <luck@lucko.me>
+ *
+ *  Permission is hereby granted, free of charge, to any person obtaining a copy
+ *  of this software and associated documentation files (the "Software"), to deal
+ *  in the Software without restriction, including without limitation the rights
+ *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ *  copies of the Software, and to permit persons to whom the Software is
+ *  furnished to do so, subject to the following conditions:
+ *
+ *  The above copyright notice and this permission notice shall be included in all
+ *  copies or substantial portions of the Software.
+ *
+ *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ *  SOFTWARE.
+ */
+
+package me.lucko.luckperms.api.event;
+
+import me.lucko.luckperms.api.Track;
+import me.lucko.luckperms.api.User;
+
+public abstract class TrackEvent extends LPEvent {
+
+    private final Track track;
+    private final User user;
+    private final String fromGroup;
+    private final String toGroup;
+
+    protected TrackEvent(String eventName, Track track, User user, String fromGroup, String toGroup) {
+        super(eventName);
+        this.track = track;
+        this.user = user;
+        this.fromGroup = fromGroup;
+        this.toGroup = toGroup;
+    }
+
+    public Track getTrack() {
+        return track;
+    }
+
+    public User getUser() {
+        return user;
+    }
+
+    public String getFromGroup() {
+        return fromGroup;
+    }
+
+    public String getToGroup() {
+        return toGroup;
+    }
+}
diff --git a/api/src/main/java/me/lucko/luckperms/api/event/UserEvent.java b/api/src/main/java/me/lucko/luckperms/api/event/UserEvent.java
new file mode 100644
index 00000000..435c41ee
--- /dev/null
+++ b/api/src/main/java/me/lucko/luckperms/api/event/UserEvent.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2016 Lucko (Luck) <luck@lucko.me>
+ *
+ *  Permission is hereby granted, free of charge, to any person obtaining a copy
+ *  of this software and associated documentation files (the "Software"), to deal
+ *  in the Software without restriction, including without limitation the rights
+ *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ *  copies of the Software, and to permit persons to whom the Software is
+ *  furnished to do so, subject to the following conditions:
+ *
+ *  The above copyright notice and this permission notice shall be included in all
+ *  copies or substantial portions of the Software.
+ *
+ *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ *  SOFTWARE.
+ */
+
+package me.lucko.luckperms.api.event;
+
+import me.lucko.luckperms.api.User;
+
+public class UserEvent extends LPEvent {
+
+    private final User user;
+
+    protected UserEvent(String eventName, User user) {
+        super(eventName);
+        this.user = user;
+    }
+
+    public User getUser() {
+        return user;
+    }
+}
diff --git a/api/src/main/java/me/lucko/luckperms/api/event/events/GroupAddEvent.java b/api/src/main/java/me/lucko/luckperms/api/event/events/GroupAddEvent.java
new file mode 100644
index 00000000..0d4836fa
--- /dev/null
+++ b/api/src/main/java/me/lucko/luckperms/api/event/events/GroupAddEvent.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2016 Lucko (Luck) <luck@lucko.me>
+ *
+ *  Permission is hereby granted, free of charge, to any person obtaining a copy
+ *  of this software and associated documentation files (the "Software"), to deal
+ *  in the Software without restriction, including without limitation the rights
+ *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ *  copies of the Software, and to permit persons to whom the Software is
+ *  furnished to do so, subject to the following conditions:
+ *
+ *  The above copyright notice and this permission notice shall be included in all
+ *  copies or substantial portions of the Software.
+ *
+ *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ *  SOFTWARE.
+ */
+
+package me.lucko.luckperms.api.event.events;
+
+import me.lucko.luckperms.api.Group;
+import me.lucko.luckperms.api.PermissionHolder;
+import me.lucko.luckperms.api.event.AbstractPermissionAddEvent;
+
+public class GroupAddEvent extends AbstractPermissionAddEvent {
+
+    private final Group group;
+
+    public GroupAddEvent(PermissionHolder target, Group group, String server, String world, long expiry) {
+        super("Group Add Event", target, server, world, expiry);
+        this.group = group;
+    }
+
+    public Group getGroup() {
+        return group;
+    }
+}
diff --git a/api/src/main/java/me/lucko/luckperms/api/event/events/GroupRemoveEvent.java b/api/src/main/java/me/lucko/luckperms/api/event/events/GroupRemoveEvent.java
new file mode 100644
index 00000000..358035b3
--- /dev/null
+++ b/api/src/main/java/me/lucko/luckperms/api/event/events/GroupRemoveEvent.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2016 Lucko (Luck) <luck@lucko.me>
+ *
+ *  Permission is hereby granted, free of charge, to any person obtaining a copy
+ *  of this software and associated documentation files (the "Software"), to deal
+ *  in the Software without restriction, including without limitation the rights
+ *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ *  copies of the Software, and to permit persons to whom the Software is
+ *  furnished to do so, subject to the following conditions:
+ *
+ *  The above copyright notice and this permission notice shall be included in all
+ *  copies or substantial portions of the Software.
+ *
+ *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ *  SOFTWARE.
+ */
+
+package me.lucko.luckperms.api.event.events;
+
+import me.lucko.luckperms.api.PermissionHolder;
+import me.lucko.luckperms.api.event.AbstractPermissionRemoveEvent;
+
+public class GroupRemoveEvent extends AbstractPermissionRemoveEvent {
+
+    private final String group;
+
+    public GroupRemoveEvent(PermissionHolder target, String group, String server, String world, boolean temporary) {
+        super("Group Remove Event", target, server, world, temporary);
+        this.group = group;
+    }
+
+    public String getGroup() {
+        return group;
+    }
+}
diff --git a/api/src/main/java/me/lucko/luckperms/api/event/events/LogNotifyEvent.java b/api/src/main/java/me/lucko/luckperms/api/event/events/LogNotifyEvent.java
new file mode 100644
index 00000000..6cfd6679
--- /dev/null
+++ b/api/src/main/java/me/lucko/luckperms/api/event/events/LogNotifyEvent.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2016 Lucko (Luck) <luck@lucko.me>
+ *
+ *  Permission is hereby granted, free of charge, to any person obtaining a copy
+ *  of this software and associated documentation files (the "Software"), to deal
+ *  in the Software without restriction, including without limitation the rights
+ *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ *  copies of the Software, and to permit persons to whom the Software is
+ *  furnished to do so, subject to the following conditions:
+ *
+ *  The above copyright notice and this permission notice shall be included in all
+ *  copies or substantial portions of the Software.
+ *
+ *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ *  SOFTWARE.
+ */
+
+package me.lucko.luckperms.api.event.events;
+
+import me.lucko.luckperms.api.LogEntry;
+import me.lucko.luckperms.api.event.CancellableEvent;
+
+public class LogNotifyEvent extends CancellableEvent {
+
+    private final LogEntry entry;
+
+    public LogNotifyEvent(LogEntry entry) {
+        super("Log Notify Event");
+        this.entry = entry;
+    }
+
+    public LogEntry getEntry() {
+        return entry;
+    }
+
+}
diff --git a/api/src/main/java/me/lucko/luckperms/api/event/events/PermissionExpireEvent.java b/api/src/main/java/me/lucko/luckperms/api/event/events/PermissionExpireEvent.java
new file mode 100644
index 00000000..4a474f3d
--- /dev/null
+++ b/api/src/main/java/me/lucko/luckperms/api/event/events/PermissionExpireEvent.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2016 Lucko (Luck) <luck@lucko.me>
+ *
+ *  Permission is hereby granted, free of charge, to any person obtaining a copy
+ *  of this software and associated documentation files (the "Software"), to deal
+ *  in the Software without restriction, including without limitation the rights
+ *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ *  copies of the Software, and to permit persons to whom the Software is
+ *  furnished to do so, subject to the following conditions:
+ *
+ *  The above copyright notice and this permission notice shall be included in all
+ *  copies or substantial portions of the Software.
+ *
+ *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ *  SOFTWARE.
+ */
+
+package me.lucko.luckperms.api.event.events;
+
+import me.lucko.luckperms.api.PermissionHolder;
+import me.lucko.luckperms.api.event.TargetedEvent;
+
+public class PermissionExpireEvent extends TargetedEvent<PermissionHolder> {
+
+    private final String node;
+
+    public PermissionExpireEvent(PermissionHolder target, String node) {
+        super("Permission Expire Event", target);
+        this.node = node;
+    }
+
+    public String getNode() {
+        return node;
+    }
+}
diff --git a/api/src/main/java/me/lucko/luckperms/api/event/events/PermissionSetEvent.java b/api/src/main/java/me/lucko/luckperms/api/event/events/PermissionSetEvent.java
new file mode 100644
index 00000000..b0b4b97e
--- /dev/null
+++ b/api/src/main/java/me/lucko/luckperms/api/event/events/PermissionSetEvent.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2016 Lucko (Luck) <luck@lucko.me>
+ *
+ *  Permission is hereby granted, free of charge, to any person obtaining a copy
+ *  of this software and associated documentation files (the "Software"), to deal
+ *  in the Software without restriction, including without limitation the rights
+ *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ *  copies of the Software, and to permit persons to whom the Software is
+ *  furnished to do so, subject to the following conditions:
+ *
+ *  The above copyright notice and this permission notice shall be included in all
+ *  copies or substantial portions of the Software.
+ *
+ *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ *  SOFTWARE.
+ */
+
+package me.lucko.luckperms.api.event.events;
+
+import me.lucko.luckperms.api.PermissionHolder;
+import me.lucko.luckperms.api.event.AbstractPermissionAddEvent;
+
+import java.util.AbstractMap;
+import java.util.Map;
+
+public class PermissionSetEvent extends AbstractPermissionAddEvent {
+
+    private final String node;
+    private final boolean value;
+
+    public PermissionSetEvent(PermissionHolder target, String node, boolean value, String server, String world, long expiry) {
+        super("Permission Set Event", target, server, world, expiry);
+        this.node = node;
+        this.value = value;
+    }
+
+    public String getNode() {
+        return node;
+    }
+
+    public boolean getValue() {
+        return value;
+    }
+
+    public Map.Entry<String, Boolean> getEntry() {
+        return new AbstractMap.SimpleEntry<>(node, value);
+    }
+}
diff --git a/api/src/main/java/me/lucko/luckperms/api/event/events/PermissionUnsetEvent.java b/api/src/main/java/me/lucko/luckperms/api/event/events/PermissionUnsetEvent.java
new file mode 100644
index 00000000..d7b2738e
--- /dev/null
+++ b/api/src/main/java/me/lucko/luckperms/api/event/events/PermissionUnsetEvent.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2016 Lucko (Luck) <luck@lucko.me>
+ *
+ *  Permission is hereby granted, free of charge, to any person obtaining a copy
+ *  of this software and associated documentation files (the "Software"), to deal
+ *  in the Software without restriction, including without limitation the rights
+ *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ *  copies of the Software, and to permit persons to whom the Software is
+ *  furnished to do so, subject to the following conditions:
+ *
+ *  The above copyright notice and this permission notice shall be included in all
+ *  copies or substantial portions of the Software.
+ *
+ *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ *  SOFTWARE.
+ */
+
+package me.lucko.luckperms.api.event.events;
+
+import me.lucko.luckperms.api.PermissionHolder;
+import me.lucko.luckperms.api.event.AbstractPermissionRemoveEvent;
+
+public class PermissionUnsetEvent extends AbstractPermissionRemoveEvent {
+
+    private final String node;
+
+    public PermissionUnsetEvent(PermissionHolder target, String node, String server, String world, boolean temporary) {
+        super("Permission Unset Event", target, server, world, temporary);
+        this.node = node;
+    }
+
+    public String getNode() {
+        return node;
+    }
+}
diff --git a/api/src/main/java/me/lucko/luckperms/api/event/events/PostSyncEvent.java b/api/src/main/java/me/lucko/luckperms/api/event/events/PostSyncEvent.java
new file mode 100644
index 00000000..a41e0190
--- /dev/null
+++ b/api/src/main/java/me/lucko/luckperms/api/event/events/PostSyncEvent.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2016 Lucko (Luck) <luck@lucko.me>
+ *
+ *  Permission is hereby granted, free of charge, to any person obtaining a copy
+ *  of this software and associated documentation files (the "Software"), to deal
+ *  in the Software without restriction, including without limitation the rights
+ *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ *  copies of the Software, and to permit persons to whom the Software is
+ *  furnished to do so, subject to the following conditions:
+ *
+ *  The above copyright notice and this permission notice shall be included in all
+ *  copies or substantial portions of the Software.
+ *
+ *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ *  SOFTWARE.
+ */
+
+package me.lucko.luckperms.api.event.events;
+
+import me.lucko.luckperms.api.event.LPEvent;
+
+public class PostSyncEvent extends LPEvent {
+
+    public PostSyncEvent() {
+        super("Post Sync Event");
+    }
+
+}
diff --git a/api/src/main/java/me/lucko/luckperms/api/event/events/PreSyncEvent.java b/api/src/main/java/me/lucko/luckperms/api/event/events/PreSyncEvent.java
new file mode 100644
index 00000000..a3817bef
--- /dev/null
+++ b/api/src/main/java/me/lucko/luckperms/api/event/events/PreSyncEvent.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2016 Lucko (Luck) <luck@lucko.me>
+ *
+ *  Permission is hereby granted, free of charge, to any person obtaining a copy
+ *  of this software and associated documentation files (the "Software"), to deal
+ *  in the Software without restriction, including without limitation the rights
+ *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ *  copies of the Software, and to permit persons to whom the Software is
+ *  furnished to do so, subject to the following conditions:
+ *
+ *  The above copyright notice and this permission notice shall be included in all
+ *  copies or substantial portions of the Software.
+ *
+ *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ *  SOFTWARE.
+ */
+
+package me.lucko.luckperms.api.event.events;
+
+import me.lucko.luckperms.api.event.CancellableEvent;
+
+public class PreSyncEvent extends CancellableEvent {
+
+    public PreSyncEvent() {
+        super("Pre Sync Event");
+    }
+
+}
diff --git a/api/src/main/java/me/lucko/luckperms/api/event/events/UserDemoteEvent.java b/api/src/main/java/me/lucko/luckperms/api/event/events/UserDemoteEvent.java
new file mode 100644
index 00000000..8902290f
--- /dev/null
+++ b/api/src/main/java/me/lucko/luckperms/api/event/events/UserDemoteEvent.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2016 Lucko (Luck) <luck@lucko.me>
+ *
+ *  Permission is hereby granted, free of charge, to any person obtaining a copy
+ *  of this software and associated documentation files (the "Software"), to deal
+ *  in the Software without restriction, including without limitation the rights
+ *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ *  copies of the Software, and to permit persons to whom the Software is
+ *  furnished to do so, subject to the following conditions:
+ *
+ *  The above copyright notice and this permission notice shall be included in all
+ *  copies or substantial portions of the Software.
+ *
+ *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ *  SOFTWARE.
+ */
+
+package me.lucko.luckperms.api.event.events;
+
+import me.lucko.luckperms.api.Track;
+import me.lucko.luckperms.api.User;
+import me.lucko.luckperms.api.event.TrackEvent;
+
+public class UserDemoteEvent extends TrackEvent {
+
+    public UserDemoteEvent(Track track, User user, String from, String to) {
+        super("User Demote Event", track, user, from, to);
+    }
+
+}
diff --git a/api/src/main/java/me/lucko/luckperms/api/event/events/UserFirstLoginEvent.java b/api/src/main/java/me/lucko/luckperms/api/event/events/UserFirstLoginEvent.java
new file mode 100644
index 00000000..d0562ce6
--- /dev/null
+++ b/api/src/main/java/me/lucko/luckperms/api/event/events/UserFirstLoginEvent.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2016 Lucko (Luck) <luck@lucko.me>
+ *
+ *  Permission is hereby granted, free of charge, to any person obtaining a copy
+ *  of this software and associated documentation files (the "Software"), to deal
+ *  in the Software without restriction, including without limitation the rights
+ *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ *  copies of the Software, and to permit persons to whom the Software is
+ *  furnished to do so, subject to the following conditions:
+ *
+ *  The above copyright notice and this permission notice shall be included in all
+ *  copies or substantial portions of the Software.
+ *
+ *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ *  SOFTWARE.
+ */
+
+package me.lucko.luckperms.api.event.events;
+
+import me.lucko.luckperms.api.event.LPEvent;
+
+import java.util.UUID;
+
+/**
+ * This event is fired before the player has actually joined the game on the async login / auth event.
+ * If you want to do something with the user, store the UUID in a set, and then check the set in the PlayerJoinEvent o.e.
+ */
+public class UserFirstLoginEvent extends LPEvent {
+
+    private final UUID uuid;
+    private final String username;
+
+    public UserFirstLoginEvent(UUID uuid, String username) {
+        super("User First Join Event");
+        this.uuid = uuid;
+        this.username = username;
+    }
+
+    public UUID getUuid() {
+        return uuid;
+    }
+
+    public String getUsername() {
+        return username;
+    }
+}
diff --git a/api/src/main/java/me/lucko/luckperms/api/event/events/UserPermissionRefreshEvent.java b/api/src/main/java/me/lucko/luckperms/api/event/events/UserPermissionRefreshEvent.java
new file mode 100644
index 00000000..60bb8cd8
--- /dev/null
+++ b/api/src/main/java/me/lucko/luckperms/api/event/events/UserPermissionRefreshEvent.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2016 Lucko (Luck) <luck@lucko.me>
+ *
+ *  Permission is hereby granted, free of charge, to any person obtaining a copy
+ *  of this software and associated documentation files (the "Software"), to deal
+ *  in the Software without restriction, including without limitation the rights
+ *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ *  copies of the Software, and to permit persons to whom the Software is
+ *  furnished to do so, subject to the following conditions:
+ *
+ *  The above copyright notice and this permission notice shall be included in all
+ *  copies or substantial portions of the Software.
+ *
+ *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ *  SOFTWARE.
+ */
+
+package me.lucko.luckperms.api.event.events;
+
+import me.lucko.luckperms.api.User;
+import me.lucko.luckperms.api.event.UserEvent;
+
+public class UserPermissionRefreshEvent extends UserEvent {
+
+    public UserPermissionRefreshEvent(User user) {
+        super("User Permission Refresh Event", user);
+    }
+
+}
diff --git a/api/src/main/java/me/lucko/luckperms/api/event/events/UserPromoteEvent.java b/api/src/main/java/me/lucko/luckperms/api/event/events/UserPromoteEvent.java
new file mode 100644
index 00000000..7bd38156
--- /dev/null
+++ b/api/src/main/java/me/lucko/luckperms/api/event/events/UserPromoteEvent.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2016 Lucko (Luck) <luck@lucko.me>
+ *
+ *  Permission is hereby granted, free of charge, to any person obtaining a copy
+ *  of this software and associated documentation files (the "Software"), to deal
+ *  in the Software without restriction, including without limitation the rights
+ *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ *  copies of the Software, and to permit persons to whom the Software is
+ *  furnished to do so, subject to the following conditions:
+ *
+ *  The above copyright notice and this permission notice shall be included in all
+ *  copies or substantial portions of the Software.
+ *
+ *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ *  SOFTWARE.
+ */
+
+package me.lucko.luckperms.api.event.events;
+
+import me.lucko.luckperms.api.Track;
+import me.lucko.luckperms.api.User;
+import me.lucko.luckperms.api.event.TrackEvent;
+
+public class UserPromoteEvent extends TrackEvent {
+
+    public UserPromoteEvent(Track track, User user, String from, String to) {
+        super("User Promote Event", track, user, from, to);
+    }
+
+}
diff --git a/bukkit/pom.xml b/bukkit/pom.xml
index 365e0174..122b936f 100644
--- a/bukkit/pom.xml
+++ b/bukkit/pom.xml
@@ -5,7 +5,7 @@
     <parent>
         <artifactId>luckperms</artifactId>
         <groupId>me.lucko.luckperms</groupId>
-        <version>2.4-SNAPSHOT</version>
+        <version>2.5-SNAPSHOT</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
diff --git a/bukkit/src/main/java/me/lucko/luckperms/LPBukkitPlugin.java b/bukkit/src/main/java/me/lucko/luckperms/LPBukkitPlugin.java
index b3a464a4..6589f405 100644
--- a/bukkit/src/main/java/me/lucko/luckperms/LPBukkitPlugin.java
+++ b/bukkit/src/main/java/me/lucko/luckperms/LPBukkitPlugin.java
@@ -60,6 +60,7 @@ public class LPBukkitPlugin extends JavaPlugin implements LuckPermsPlugin {
     private TrackManager trackManager;
     private Datastore datastore;
     private UuidCache uuidCache;
+    private ApiProvider apiProvider;
     private Logger log;
     private Importer importer;
 
@@ -123,9 +124,9 @@ public class LPBukkitPlugin extends JavaPlugin implements LuckPermsPlugin {
         }
 
         getLog().info("Registering API...");
-        final ApiProvider provider = new ApiProvider(this);
-        LuckPerms.registerProvider(provider);
-        getServer().getServicesManager().register(LuckPermsApi.class, provider, this, ServicePriority.Normal);
+        apiProvider = new ApiProvider(this);
+        LuckPerms.registerProvider(apiProvider);
+        getServer().getServicesManager().register(LuckPermsApi.class, apiProvider, this, ServicePriority.Normal);
 
         getLog().info("Successfully loaded.");
     }
diff --git a/bukkit/src/main/java/me/lucko/luckperms/users/BukkitUser.java b/bukkit/src/main/java/me/lucko/luckperms/users/BukkitUser.java
index 3e1c3fb9..4bbe03b6 100644
--- a/bukkit/src/main/java/me/lucko/luckperms/users/BukkitUser.java
+++ b/bukkit/src/main/java/me/lucko/luckperms/users/BukkitUser.java
@@ -25,6 +25,8 @@ package me.lucko.luckperms.users;
 import lombok.Getter;
 import lombok.Setter;
 import me.lucko.luckperms.LPBukkitPlugin;
+import me.lucko.luckperms.api.event.events.UserPermissionRefreshEvent;
+import me.lucko.luckperms.api.implementation.internal.UserLink;
 import org.bukkit.entity.Player;
 import org.bukkit.permissions.PermissionAttachment;
 
@@ -104,6 +106,8 @@ public class BukkitUser extends User {
             } catch (Exception e) {
                 e.printStackTrace();
             }
+
+            plugin.getApiProvider().fireEventAsync(new UserPermissionRefreshEvent(new UserLink(this)));
         });
     }
 }
diff --git a/bungee/pom.xml b/bungee/pom.xml
index 1e4f60de..144c7f81 100644
--- a/bungee/pom.xml
+++ b/bungee/pom.xml
@@ -5,7 +5,7 @@
     <parent>
         <artifactId>luckperms</artifactId>
         <groupId>me.lucko.luckperms</groupId>
-        <version>2.4-SNAPSHOT</version>
+        <version>2.5-SNAPSHOT</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
diff --git a/bungee/src/main/java/me/lucko/luckperms/LPBungeePlugin.java b/bungee/src/main/java/me/lucko/luckperms/LPBungeePlugin.java
index c7dc87ca..da785968 100644
--- a/bungee/src/main/java/me/lucko/luckperms/LPBungeePlugin.java
+++ b/bungee/src/main/java/me/lucko/luckperms/LPBungeePlugin.java
@@ -57,6 +57,7 @@ public class LPBungeePlugin extends Plugin implements LuckPermsPlugin {
     private TrackManager trackManager;
     private Datastore datastore;
     private UuidCache uuidCache;
+    private ApiProvider apiProvider;
     private Logger log;
     private Importer importer;
 
@@ -105,7 +106,8 @@ public class LPBungeePlugin extends Plugin implements LuckPermsPlugin {
         getProxy().getScheduler().schedule(this, new ExpireTemporaryTask(this), 3L, 3L, TimeUnit.SECONDS);
 
         getLog().info("Registering API...");
-        LuckPerms.registerProvider(new ApiProvider(this));
+        apiProvider = new ApiProvider(this);
+        LuckPerms.registerProvider(apiProvider);
 
         getLog().info("Successfully loaded.");
     }
diff --git a/common/pom.xml b/common/pom.xml
index 2fbfe368..6c3303dc 100644
--- a/common/pom.xml
+++ b/common/pom.xml
@@ -5,7 +5,7 @@
     <parent>
         <artifactId>luckperms</artifactId>
         <groupId>me.lucko.luckperms</groupId>
-        <version>2.4-SNAPSHOT</version>
+        <version>2.5-SNAPSHOT</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
diff --git a/common/src/main/java/me/lucko/luckperms/LuckPermsPlugin.java b/common/src/main/java/me/lucko/luckperms/LuckPermsPlugin.java
index ee98bafa..e339f9f8 100644
--- a/common/src/main/java/me/lucko/luckperms/LuckPermsPlugin.java
+++ b/common/src/main/java/me/lucko/luckperms/LuckPermsPlugin.java
@@ -23,6 +23,7 @@
 package me.lucko.luckperms;
 
 import me.lucko.luckperms.api.Logger;
+import me.lucko.luckperms.api.implementation.ApiProvider;
 import me.lucko.luckperms.commands.Sender;
 import me.lucko.luckperms.constants.Message;
 import me.lucko.luckperms.core.LPConfiguration;
@@ -44,47 +45,18 @@ import java.util.UUID;
  */
 public interface LuckPermsPlugin {
 
-    /**
-     * Retrieves the {@link UserManager} used to manage users and their permissions/groups
-     * @return the {@link UserManager} instance
+    /*
+     * Access to all of the main internal manager classes
      */
     UserManager getUserManager();
-
-    /**
-     * Retrieves the {@link GroupManager} used to manage the loaded groups and modify their permissions
-     * @return the {@link GroupManager} instance
-     */
     GroupManager getGroupManager();
-
-    /**
-     * Retrieves the {@link TrackManager} used to manage the loaded tracks
-     * @return the {@link TrackManager} instance
-     */
     TrackManager getTrackManager();
-
-    /**
-     * Retrieves the {@link LPConfiguration} for getting values from the config
-     * @return the {@link LPConfiguration} implementation for the platform
-     */
     LPConfiguration getConfiguration();
-
-    /**
-     * Retrieves the {@link Datastore} for loading/saving plugin data
-     * @return the {@link Datastore} object
-     */
     Datastore getDatastore();
-
-    /**
-     * Retrieves the {@link Logger} for the plugin
-     * @return the plugin's {@link Logger}
-     */
     Logger getLog();
-
-    /**
-     * Retrieves the {@link UuidCache} for the plugin
-     * @return the plugin's {@link UuidCache}
-     */
     UuidCache getUuidCache();
+    ApiProvider getApiProvider();
+    Importer getImporter();
 
     /**
      * @return the version of the plugin
@@ -101,11 +73,6 @@ public interface LuckPermsPlugin {
      */
     File getDataFolder();
 
-    /**
-     * @return the importer instance for the platform
-     */
-    Importer getImporter();
-
     /**
      * Returns a colored string indicating the status of a player
      * @param uuid The player's uuid
@@ -126,14 +93,12 @@ public interface LuckPermsPlugin {
     List<String> getPlayerList();
 
     /**
-     * Gets a list of all Senders online on the platform
-     * @return a {@link List} of senders
+     * @return a {@link List} of senders online on the platform
      */
     List<Sender> getSenders();
 
     /**
-     * Gets the console sender of the instance
-     * @return a the console sender of the instance
+     * @return the console sender of the instance
      */
     Sender getConsoleSender();
 
diff --git a/common/src/main/java/me/lucko/luckperms/api/implementation/ApiProvider.java b/common/src/main/java/me/lucko/luckperms/api/implementation/ApiProvider.java
index af80c173..173f5ba5 100644
--- a/common/src/main/java/me/lucko/luckperms/api/implementation/ApiProvider.java
+++ b/common/src/main/java/me/lucko/luckperms/api/implementation/ApiProvider.java
@@ -22,10 +22,13 @@
 
 package me.lucko.luckperms.api.implementation;
 
+import com.google.common.eventbus.EventBus;
 import lombok.AllArgsConstructor;
 import lombok.NonNull;
 import me.lucko.luckperms.LuckPermsPlugin;
 import me.lucko.luckperms.api.*;
+import me.lucko.luckperms.api.event.LPEvent;
+import me.lucko.luckperms.api.event.LPListener;
 import me.lucko.luckperms.api.implementation.internal.*;
 
 import java.util.Optional;
@@ -39,6 +42,21 @@ import java.util.stream.Collectors;
 @AllArgsConstructor
 public class ApiProvider implements LuckPermsApi {
     private final LuckPermsPlugin plugin;
+    private final EventBus eventBus = new EventBus("LuckPerms");
+
+    public void fireEventAsync(LPEvent event) {
+        plugin.doAsync(() -> fireEvent(event));
+    }
+
+    public void fireEvent(LPEvent event) {
+        try {
+            event.setApi(this);
+            eventBus.post(event);
+        } catch (Exception e) {
+            getLogger().severe("Couldn't fire LuckPerms Event: " + event.getEventName());
+            e.printStackTrace();
+        }
+    }
 
     @Override
     public void runUpdateTask() {
@@ -50,6 +68,16 @@ public class ApiProvider implements LuckPermsApi {
         return plugin.getVersion();
     }
 
+    @Override
+    public void registerListener(LPListener listener) {
+        eventBus.register(listener);
+    }
+
+    @Override
+    public void unregisterListener(LPListener listener) {
+        eventBus.unregister(listener);
+    }
+
     @Override
     public LPConfiguration getConfiguration() {
         return new LPConfigurationLink(plugin.getConfiguration());
diff --git a/common/src/main/java/me/lucko/luckperms/api/implementation/internal/PermissionHolderLink.java b/common/src/main/java/me/lucko/luckperms/api/implementation/internal/PermissionHolderLink.java
index 2ababe9a..2b21a28c 100644
--- a/common/src/main/java/me/lucko/luckperms/api/implementation/internal/PermissionHolderLink.java
+++ b/common/src/main/java/me/lucko/luckperms/api/implementation/internal/PermissionHolderLink.java
@@ -22,7 +22,6 @@
 
 package me.lucko.luckperms.api.implementation.internal;
 
-import lombok.AccessLevel;
 import lombok.AllArgsConstructor;
 import lombok.NonNull;
 import me.lucko.luckperms.api.PermissionHolder;
@@ -39,8 +38,8 @@ import static me.lucko.luckperms.api.implementation.internal.Utils.*;
  * Provides a link between {@link PermissionHolder} and {@link me.lucko.luckperms.core.PermissionHolder}
  */
 @SuppressWarnings("unused")
-@AllArgsConstructor(access = AccessLevel.PACKAGE)
-class PermissionHolderLink implements PermissionHolder {
+@AllArgsConstructor
+public class PermissionHolderLink implements PermissionHolder {
 
     @NonNull
     private final me.lucko.luckperms.core.PermissionHolder master;
diff --git a/common/src/main/java/me/lucko/luckperms/commands/user/subcommands/UserDemote.java b/common/src/main/java/me/lucko/luckperms/commands/user/subcommands/UserDemote.java
index f24cf716..99645e1c 100644
--- a/common/src/main/java/me/lucko/luckperms/commands/user/subcommands/UserDemote.java
+++ b/common/src/main/java/me/lucko/luckperms/commands/user/subcommands/UserDemote.java
@@ -23,6 +23,9 @@
 package me.lucko.luckperms.commands.user.subcommands;
 
 import me.lucko.luckperms.LuckPermsPlugin;
+import me.lucko.luckperms.api.event.events.UserDemoteEvent;
+import me.lucko.luckperms.api.implementation.internal.TrackLink;
+import me.lucko.luckperms.api.implementation.internal.UserLink;
 import me.lucko.luckperms.commands.*;
 import me.lucko.luckperms.constants.Message;
 import me.lucko.luckperms.constants.Permission;
@@ -107,6 +110,7 @@ public class UserDemote extends SubCommand<User> {
                 .action("demote " + track.getName() + "(from " + old + " to " + previousGroup.getName() + ")")
                 .build().submit(plugin);
         save(user, sender, plugin);
+        plugin.getApiProvider().fireEventAsync(new UserDemoteEvent(new TrackLink(track), new UserLink(user), old, previousGroup.getName()));
         return CommandResult.SUCCESS;
     }
 
diff --git a/common/src/main/java/me/lucko/luckperms/commands/user/subcommands/UserPromote.java b/common/src/main/java/me/lucko/luckperms/commands/user/subcommands/UserPromote.java
index 1f07b68b..e349a897 100644
--- a/common/src/main/java/me/lucko/luckperms/commands/user/subcommands/UserPromote.java
+++ b/common/src/main/java/me/lucko/luckperms/commands/user/subcommands/UserPromote.java
@@ -23,6 +23,9 @@
 package me.lucko.luckperms.commands.user.subcommands;
 
 import me.lucko.luckperms.LuckPermsPlugin;
+import me.lucko.luckperms.api.event.events.UserPromoteEvent;
+import me.lucko.luckperms.api.implementation.internal.TrackLink;
+import me.lucko.luckperms.api.implementation.internal.UserLink;
 import me.lucko.luckperms.commands.*;
 import me.lucko.luckperms.constants.Message;
 import me.lucko.luckperms.constants.Permission;
@@ -107,6 +110,7 @@ public class UserPromote extends SubCommand<User> {
                 .action("promote " + track.getName() + "(from " + old + " to " + nextGroup.getName() + ")")
                 .build().submit(plugin);
         save(user, sender, plugin);
+        plugin.getApiProvider().fireEventAsync(new UserPromoteEvent(new TrackLink(track), new UserLink(user), old, nextGroup.getName()));
         return CommandResult.SUCCESS;
     }
 
diff --git a/common/src/main/java/me/lucko/luckperms/core/PermissionHolder.java b/common/src/main/java/me/lucko/luckperms/core/PermissionHolder.java
index 57ab2746..9bfdbc49 100644
--- a/common/src/main/java/me/lucko/luckperms/core/PermissionHolder.java
+++ b/common/src/main/java/me/lucko/luckperms/core/PermissionHolder.java
@@ -26,6 +26,11 @@ import lombok.AccessLevel;
 import lombok.Getter;
 import lombok.RequiredArgsConstructor;
 import me.lucko.luckperms.LuckPermsPlugin;
+import me.lucko.luckperms.api.event.events.GroupRemoveEvent;
+import me.lucko.luckperms.api.event.events.PermissionExpireEvent;
+import me.lucko.luckperms.api.event.events.PermissionSetEvent;
+import me.lucko.luckperms.api.event.events.PermissionUnsetEvent;
+import me.lucko.luckperms.api.implementation.internal.PermissionHolderLink;
 import me.lucko.luckperms.constants.Patterns;
 import me.lucko.luckperms.exceptions.ObjectAlreadyHasException;
 import me.lucko.luckperms.exceptions.ObjectLacksException;
@@ -252,11 +257,7 @@ public abstract class PermissionHolder {
      * @throws ObjectAlreadyHasException if the object already has the permission
      */
     public void setPermission(String node, boolean value) throws ObjectAlreadyHasException {
-        if (node.startsWith("global/")) node = node.replace("global/", "");
-        if (hasPermission(node, value)) {
-            throw new ObjectAlreadyHasException();
-        }
-        this.nodes.put(node, value);
+        setPermission(node, value, null, null, 0L);
     }
 
     /**
@@ -267,7 +268,7 @@ public abstract class PermissionHolder {
      * @throws ObjectAlreadyHasException if the object already has the permission
      */
     public void setPermission(String node, boolean value, String server) throws ObjectAlreadyHasException {
-        setPermission(server + "/" + node, value);
+        setPermission(node, value, server, null, 0L);
     }
 
     /**
@@ -279,7 +280,7 @@ public abstract class PermissionHolder {
      * @throws ObjectAlreadyHasException if the object already has the permission
      */
     public void setPermission(String node, boolean value, String server, String world) throws ObjectAlreadyHasException {
-        setPermission(server + "-" + world + "/" + node, value);
+        setPermission(node, value, server, world, 0L);
     }
 
     /**
@@ -290,7 +291,7 @@ public abstract class PermissionHolder {
      * @throws ObjectAlreadyHasException if the object already has the permission
      */
     public void setPermission(String node, boolean value, long expireAt) throws ObjectAlreadyHasException {
-        setPermission(node + "$" + expireAt, value);
+        setPermission(node, value, null, null, expireAt);
     }
 
     /**
@@ -302,7 +303,7 @@ public abstract class PermissionHolder {
      * @throws ObjectAlreadyHasException if the object already has the permission
      */
     public void setPermission(String node, boolean value, String server, long expireAt) throws ObjectAlreadyHasException {
-        setPermission(node + "$" + expireAt, value, server);
+        setPermission(node, value, server, null, expireAt);
     }
 
     /**
@@ -315,7 +316,43 @@ public abstract class PermissionHolder {
      * @throws ObjectAlreadyHasException if the object already has the permission
      */
     public void setPermission(String node, boolean value, String server, String world, long expireAt) throws ObjectAlreadyHasException {
-        setPermission(node + "$" + expireAt, value, server, world);
+        if (node.startsWith("global/")) node = node.replace("global/", "");
+
+        if (server != null && server.equals("")) server = null;
+        if (world != null && world.equals("")) world = null;
+
+        StringBuilder builder = new StringBuilder();
+
+        if (server != null) {
+            builder.append(server);
+
+            if (world != null) {
+                builder.append("-").append(world);
+            }
+            builder.append("/");
+        } else {
+            if (world != null) {
+                builder.append("global-").append(world);
+            }
+            builder.append("/");
+        }
+
+        builder.append(node);
+
+        if (expireAt != 0L) {
+            builder.append("$").append(expireAt);
+        }
+
+        final String finalNode = builder.toString();
+
+
+        if (hasPermission(node, value)) {
+            throw new ObjectAlreadyHasException();
+        }
+
+        this.nodes.put(finalNode, value);
+        plugin.getApiProvider().fireEventAsync(new PermissionSetEvent(
+                new PermissionHolderLink(this), node, value, server, world, expireAt));
     }
 
     /**
@@ -325,26 +362,7 @@ public abstract class PermissionHolder {
      * @throws ObjectLacksException if the node wasn't already set
      */
     public void unsetPermission(String node, boolean temporary) throws ObjectLacksException {
-        if (node.startsWith("global/")) node = node.replace("global/", "");
-        final String fNode = node;
-        Optional<String> match = Optional.empty();
-
-        if (temporary) {
-            match = this.nodes.keySet().stream()
-                    .filter(n -> n.contains("$"))
-                    .filter(n -> Patterns.TEMP_DELIMITER.split(n)[0].equalsIgnoreCase(fNode))
-                    .findFirst();
-        } else {
-            if (this.nodes.containsKey(fNode)) {
-                match = Optional.of(fNode);
-            }
-        }
-
-        if (match.isPresent()) {
-            this.nodes.remove(match.get());
-        } else {
-            throw new ObjectLacksException();
-        }
+        unsetPermission(node, null, null, temporary);
     }
 
     /**
@@ -353,7 +371,7 @@ public abstract class PermissionHolder {
      * @throws ObjectLacksException if the node wasn't already set
      */
     public void unsetPermission(String node) throws ObjectLacksException {
-        unsetPermission(node, node.contains("$"));
+        unsetPermission(node, null, null, false);
     }
 
     /**
@@ -363,7 +381,7 @@ public abstract class PermissionHolder {
      * @throws ObjectLacksException if the node wasn't already set
      */
     public void unsetPermission(String node, String server) throws ObjectLacksException {
-        unsetPermission(server + "/" + node);
+        unsetPermission(node, server, null, false);
     }
 
     /**
@@ -374,7 +392,7 @@ public abstract class PermissionHolder {
      * @throws ObjectLacksException if the node wasn't already set
      */
     public void unsetPermission(String node, String server, String world) throws ObjectLacksException {
-        unsetPermission(server + "-" + world + "/" + node);
+        unsetPermission(node, server, world, false);
     }
 
     /**
@@ -385,7 +403,7 @@ public abstract class PermissionHolder {
      * @throws ObjectLacksException if the node wasn't already set
      */
     public void unsetPermission(String node, String server, boolean temporary) throws ObjectLacksException {
-        unsetPermission(server + "/" + node, temporary);
+        unsetPermission(node, server, null, temporary);
     }
 
     /**
@@ -397,7 +415,55 @@ public abstract class PermissionHolder {
      * @throws ObjectLacksException if the node wasn't already set
      */
     public void unsetPermission(String node, String server, String world, boolean temporary) throws ObjectLacksException {
-        unsetPermission(server + "-" + world + "/" + node, temporary);
+        if (node.startsWith("global/")) node = node.replace("global/", "");
+
+        if (server != null && server.equals("")) server = null;
+        if (world != null && world.equals("")) world = null;
+
+        StringBuilder builder = new StringBuilder();
+
+        if (server != null) {
+            builder.append(server);
+
+            if (world != null) {
+                builder.append("-").append(world);
+            }
+            builder.append("/");
+        } else {
+            if (world != null) {
+                builder.append("global-").append(world);
+            }
+            builder.append("/");
+        }
+
+        builder.append(node);
+
+        final String finalNode = builder.toString();
+        Optional<String> match = Optional.empty();
+
+        if (temporary) {
+            match = this.nodes.keySet().stream()
+                    .filter(n -> n.contains("$"))
+                    .filter(n -> Patterns.TEMP_DELIMITER.split(n)[0].equalsIgnoreCase(finalNode))
+                    .findFirst();
+        } else {
+            if (this.nodes.containsKey(finalNode)) {
+                match = Optional.of(finalNode);
+            }
+        }
+
+        if (match.isPresent()) {
+            this.nodes.remove(match.get());
+            plugin.getApiProvider().fireEventAsync(new PermissionUnsetEvent(
+                    new PermissionHolderLink(this), node, server, world, temporary));
+            if (node.startsWith("group.")) {
+                plugin.getApiProvider().fireEventAsync(new GroupRemoveEvent(
+                        new PermissionHolderLink(this), Patterns.DOT.split(node, 2)[1], server, world, temporary));
+            }
+
+        } else {
+            throw new ObjectLacksException();
+        }
     }
 
     /**
@@ -476,7 +542,10 @@ public abstract class PermissionHolder {
                 .filter(s -> DateUtil.shouldExpire(Long.parseLong(Patterns.TEMP_DELIMITER.split(s)[1])))
                 .collect(Collectors.toList());
 
-        toExpire.forEach(s -> this.nodes.remove(s));
+        toExpire.forEach(s -> {
+            plugin.getApiProvider().fireEventAsync(new PermissionExpireEvent(new PermissionHolderLink(this), s));
+            this.nodes.remove(s);
+        });
         return !toExpire.isEmpty();
     }
 
diff --git a/common/src/main/java/me/lucko/luckperms/data/LogEntry.java b/common/src/main/java/me/lucko/luckperms/data/LogEntry.java
index 3015d760..3fa8ec62 100644
--- a/common/src/main/java/me/lucko/luckperms/data/LogEntry.java
+++ b/common/src/main/java/me/lucko/luckperms/data/LogEntry.java
@@ -23,6 +23,7 @@
 package me.lucko.luckperms.data;
 
 import me.lucko.luckperms.LuckPermsPlugin;
+import me.lucko.luckperms.api.event.events.LogNotifyEvent;
 import me.lucko.luckperms.commands.Sender;
 import me.lucko.luckperms.constants.Message;
 import me.lucko.luckperms.constants.Permission;
@@ -46,6 +47,10 @@ public class LogEntry extends me.lucko.luckperms.api.LogEntry {
     public void submit(LuckPermsPlugin plugin) {
         plugin.getDatastore().logAction(this);
 
+        LogNotifyEvent event = new LogNotifyEvent(this);
+        plugin.getApiProvider().fireEvent(event);
+        if (event.isCancelled()) return;
+
         final String msg = super.getFormatted();
 
         List<Sender> senders = plugin.getSenders().stream()
diff --git a/common/src/main/java/me/lucko/luckperms/groups/Group.java b/common/src/main/java/me/lucko/luckperms/groups/Group.java
index 48b82485..286e0bc9 100644
--- a/common/src/main/java/me/lucko/luckperms/groups/Group.java
+++ b/common/src/main/java/me/lucko/luckperms/groups/Group.java
@@ -26,6 +26,9 @@ import lombok.EqualsAndHashCode;
 import lombok.Getter;
 import lombok.ToString;
 import me.lucko.luckperms.LuckPermsPlugin;
+import me.lucko.luckperms.api.event.events.GroupAddEvent;
+import me.lucko.luckperms.api.implementation.internal.GroupLink;
+import me.lucko.luckperms.api.implementation.internal.PermissionHolderLink;
 import me.lucko.luckperms.constants.Patterns;
 import me.lucko.luckperms.core.PermissionHolder;
 import me.lucko.luckperms.exceptions.ObjectAlreadyHasException;
@@ -92,7 +95,12 @@ public class Group extends PermissionHolder implements Identifiable<String> {
      * @throws ObjectAlreadyHasException if the group already inherits the group
      */
     public void setInheritGroup(Group group) throws ObjectAlreadyHasException {
-        setInheritGroup(group, "global");
+        if (group.getName().equalsIgnoreCase(this.getName())) {
+            throw new ObjectAlreadyHasException();
+        }
+
+        setPermission("group." + group.getName(), true);
+        getPlugin().getApiProvider().fireEventAsync(new GroupAddEvent(new PermissionHolderLink(this), new GroupLink(group), null, null, 0L));
     }
 
     /**
@@ -111,6 +119,7 @@ public class Group extends PermissionHolder implements Identifiable<String> {
         }
 
         setPermission("group." + group.getName(), true, server);
+        getPlugin().getApiProvider().fireEventAsync(new GroupAddEvent(new PermissionHolderLink(this), new GroupLink(group), server, null, 0L));
     }
 
     /**
@@ -130,6 +139,7 @@ public class Group extends PermissionHolder implements Identifiable<String> {
         }
 
         setPermission("group." + group.getName(), true, server, world);
+        getPlugin().getApiProvider().fireEventAsync(new GroupAddEvent(new PermissionHolderLink(this), new GroupLink(group), server, world, 0L));
     }
 
     /**
@@ -144,6 +154,7 @@ public class Group extends PermissionHolder implements Identifiable<String> {
         }
 
         setPermission("group." + group.getName(), true, expireAt);
+        getPlugin().getApiProvider().fireEventAsync(new GroupAddEvent(new PermissionHolderLink(this), new GroupLink(group), null, null, expireAt));
     }
 
     /**
@@ -163,6 +174,7 @@ public class Group extends PermissionHolder implements Identifiable<String> {
         }
 
         setPermission("group." + group.getName(), true, server, expireAt);
+        getPlugin().getApiProvider().fireEventAsync(new GroupAddEvent(new PermissionHolderLink(this), new GroupLink(group), server, null, expireAt));
     }
 
     /**
@@ -183,6 +195,7 @@ public class Group extends PermissionHolder implements Identifiable<String> {
         }
 
         setPermission("group." + group.getName(), true, server, world, expireAt);
+        getPlugin().getApiProvider().fireEventAsync(new GroupAddEvent(new PermissionHolderLink(this), new GroupLink(group), server, world, expireAt));
     }
 
     /**
diff --git a/common/src/main/java/me/lucko/luckperms/runnables/UpdateTask.java b/common/src/main/java/me/lucko/luckperms/runnables/UpdateTask.java
index 51539f80..00ef06f5 100644
--- a/common/src/main/java/me/lucko/luckperms/runnables/UpdateTask.java
+++ b/common/src/main/java/me/lucko/luckperms/runnables/UpdateTask.java
@@ -24,13 +24,22 @@ package me.lucko.luckperms.runnables;
 
 import lombok.AllArgsConstructor;
 import me.lucko.luckperms.LuckPermsPlugin;
+import me.lucko.luckperms.api.event.events.PostSyncEvent;
+import me.lucko.luckperms.api.event.events.PreSyncEvent;
 
 @AllArgsConstructor
 public class UpdateTask implements Runnable {
     private final LuckPermsPlugin plugin;
 
+    /**
+     * Called ASYNC
+     */
     @Override
     public void run() {
+        PreSyncEvent event = new PreSyncEvent();
+        plugin.getApiProvider().fireEvent(event);
+        if (event.isCancelled()) return;
+
         // Reload all groups
         plugin.getDatastore().loadAllGroups();
         String defaultGroup = plugin.getConfiguration().getDefaultGroupName();
@@ -43,5 +52,7 @@ public class UpdateTask implements Runnable {
 
         // Refresh all online users.
         plugin.getUserManager().updateAllUsers();
+
+        plugin.getApiProvider().fireEvent(new PostSyncEvent());;
     }
 }
diff --git a/common/src/main/java/me/lucko/luckperms/users/User.java b/common/src/main/java/me/lucko/luckperms/users/User.java
index 8b707dd4..26fdcc50 100644
--- a/common/src/main/java/me/lucko/luckperms/users/User.java
+++ b/common/src/main/java/me/lucko/luckperms/users/User.java
@@ -27,6 +27,9 @@ import lombok.Getter;
 import lombok.Setter;
 import lombok.ToString;
 import me.lucko.luckperms.LuckPermsPlugin;
+import me.lucko.luckperms.api.event.events.GroupAddEvent;
+import me.lucko.luckperms.api.implementation.internal.GroupLink;
+import me.lucko.luckperms.api.implementation.internal.PermissionHolderLink;
 import me.lucko.luckperms.constants.Patterns;
 import me.lucko.luckperms.core.PermissionHolder;
 import me.lucko.luckperms.exceptions.ObjectAlreadyHasException;
@@ -121,7 +124,8 @@ public abstract class User extends PermissionHolder implements Identifiable<UUID
      * @throws ObjectAlreadyHasException if the user is already a member of the group
      */
     public void addGroup(Group group) throws ObjectAlreadyHasException {
-        addGroup(group, "global");
+        setPermission("group." + group.getName(), true);
+        getPlugin().getApiProvider().fireEventAsync(new GroupAddEvent(new PermissionHolderLink(this), new GroupLink(group), null, null, 0L));
     }
 
     /**
@@ -136,6 +140,7 @@ public abstract class User extends PermissionHolder implements Identifiable<UUID
         }
 
         setPermission("group." + group.getName(), true, server);
+        getPlugin().getApiProvider().fireEventAsync(new GroupAddEvent(new PermissionHolderLink(this), new GroupLink(group), server, null, 0L));
     }
 
     /**
@@ -151,6 +156,7 @@ public abstract class User extends PermissionHolder implements Identifiable<UUID
         }
 
         setPermission("group." + group.getName(), true, server, world);
+        getPlugin().getApiProvider().fireEventAsync(new GroupAddEvent(new PermissionHolderLink(this), new GroupLink(group), server, world, 0L));
     }
 
     /**
@@ -161,6 +167,7 @@ public abstract class User extends PermissionHolder implements Identifiable<UUID
      */
     public void addGroup(Group group, long expireAt) throws ObjectAlreadyHasException {
         setPermission("group." + group.getName(), true, expireAt);
+        getPlugin().getApiProvider().fireEventAsync(new GroupAddEvent(new PermissionHolderLink(this), new GroupLink(group), null, null, expireAt));
     }
 
     /**
@@ -176,6 +183,7 @@ public abstract class User extends PermissionHolder implements Identifiable<UUID
         }
 
         setPermission("group." + group.getName(), true, server, expireAt);
+        getPlugin().getApiProvider().fireEventAsync(new GroupAddEvent(new PermissionHolderLink(this), new GroupLink(group), server, null, expireAt));
     }
 
     /**
@@ -192,6 +200,7 @@ public abstract class User extends PermissionHolder implements Identifiable<UUID
         }
 
         setPermission("group." + group.getName(), true, server, world, expireAt);
+        getPlugin().getApiProvider().fireEventAsync(new GroupAddEvent(new PermissionHolderLink(this), new GroupLink(group), server, world, expireAt));
     }
 
     /**
diff --git a/common/src/main/java/me/lucko/luckperms/utils/AbstractListener.java b/common/src/main/java/me/lucko/luckperms/utils/AbstractListener.java
index 8776892c..188ddb02 100644
--- a/common/src/main/java/me/lucko/luckperms/utils/AbstractListener.java
+++ b/common/src/main/java/me/lucko/luckperms/utils/AbstractListener.java
@@ -25,6 +25,7 @@ package me.lucko.luckperms.utils;
 import lombok.AllArgsConstructor;
 import me.lucko.luckperms.LuckPermsPlugin;
 import me.lucko.luckperms.api.data.Callback;
+import me.lucko.luckperms.api.event.events.UserFirstLoginEvent;
 import me.lucko.luckperms.core.UuidCache;
 import me.lucko.luckperms.users.User;
 
@@ -44,10 +45,16 @@ public class AbstractListener {
                 cache.addToCache(u, uuid);
             } else {
                 // No previous data for this player
+                plugin.getApiProvider().fireEventAsync(new UserFirstLoginEvent(u, username));
                 cache.addToCache(u, u);
                 plugin.getDatastore().saveUUIDData(username, u, Callback.empty());
             }
         } else {
+            UUID uuid = plugin.getDatastore().getUUID(username);
+            if (uuid == null) {
+                plugin.getApiProvider().fireEventAsync(new UserFirstLoginEvent(u, username));
+            }
+
             // Online mode, no cache needed. This is just for name -> uuid lookup.
             plugin.getDatastore().saveUUIDData(username, u, Callback.empty());
         }
diff --git a/pom.xml b/pom.xml
index 6e534c32..13a92c44 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
 
     <groupId>me.lucko.luckperms</groupId>
     <artifactId>luckperms</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
     <modules>
         <module>common</module>
         <module>api</module>
@@ -18,7 +18,7 @@
     <packaging>pom</packaging>
     <properties>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
-        <release.version>2.4</release.version>
+        <release.version>2.5</release.version>
     </properties>
 
     <distributionManagement>
diff --git a/sponge/pom.xml b/sponge/pom.xml
index 5c1dd207..9770299c 100644
--- a/sponge/pom.xml
+++ b/sponge/pom.xml
@@ -5,7 +5,7 @@
     <parent>
         <artifactId>luckperms</artifactId>
         <groupId>me.lucko.luckperms</groupId>
-        <version>2.4-SNAPSHOT</version>
+        <version>2.5-SNAPSHOT</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
diff --git a/sponge/src/main/java/me/lucko/luckperms/LPSpongePlugin.java b/sponge/src/main/java/me/lucko/luckperms/LPSpongePlugin.java
index 0e77c9ab..ef5e10ea 100644
--- a/sponge/src/main/java/me/lucko/luckperms/LPSpongePlugin.java
+++ b/sponge/src/main/java/me/lucko/luckperms/LPSpongePlugin.java
@@ -86,6 +86,7 @@ public class LPSpongePlugin implements LuckPermsPlugin {
     private TrackManager trackManager;
     private Datastore datastore;
     private UuidCache uuidCache;
+    private ApiProvider apiProvider;
     private me.lucko.luckperms.api.Logger log;
     private Importer importer;
 
@@ -132,9 +133,9 @@ public class LPSpongePlugin implements LuckPermsPlugin {
         scheduler.createTaskBuilder().async().intervalTicks(60L).execute(new ExpireTemporaryTask(this)).submit(this);
 
         getLog().info("Registering API...");
-        final ApiProvider provider = new ApiProvider(this);
-        LuckPerms.registerProvider(provider);
-        Sponge.getServiceManager().setProvider(this, LuckPermsApi.class, provider);
+        apiProvider = new ApiProvider(this);
+        LuckPerms.registerProvider(apiProvider);
+        Sponge.getServiceManager().setProvider(this, LuckPermsApi.class, apiProvider);
 
         getLog().info("Successfully loaded.");
     }