feat: LeanEngine

oneRain 2021-03-18 13:52:23 +08:00
parent e5b9f29575
commit aad4e7cc47
14 changed files with 654 additions and 0 deletions

View File

@ -0,0 +1,31 @@
using System;
namespace LeanCloud.Engine {
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class LCEngineFunctionAttribute : Attribute {
public string FunctionName {
get;
}
public LCEngineFunctionAttribute(string funcName) {
if (string.IsNullOrEmpty(funcName)) {
throw new ArgumentNullException(nameof(funcName));
}
FunctionName = funcName;
}
}
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
public class LCEngineFunctionParameterAttribute : Attribute {
public string ParameterName {
get;
}
public LCEngineFunctionParameterAttribute(string paramName) {
if (string.IsNullOrEmpty(paramName)) {
throw new ArgumentNullException(nameof(paramName));
}
ParameterName = paramName;
}
}
}

View File

@ -0,0 +1,27 @@
using System;
namespace LeanCloud.Engine {
public enum LCEngineObjectHookType {
BeforeSave,
AfterSave,
BeforeUpdate,
AfterUpdate,
BeforeDelete,
AfterDelete
}
public class LCEngineObjectHookAttribute : Attribute {
public string ClassName {
get;
}
public LCEngineObjectHookType HookType {
get;
}
public LCEngineObjectHookAttribute(string className, LCEngineObjectHookType hookType) {
ClassName = className;
HookType = hookType;
}
}
}

View File

@ -0,0 +1,32 @@
using System;
namespace LeanCloud.Engine {
public enum LCEngineRealtimeHookType {
// 消息
MessageReceived,
MessageSent,
MessageUpdate,
ReceiversOffline,
// 对话
ConversationStart,
ConversationStarted,
ConversationAdd,
ConversationAdded,
ConversationRemove,
ConversationRemoved,
ConversationUpdate,
// 客户端
ClientOnline,
ClientOffline,
}
public class LCEngineRealtimeHookAttribute : Attribute {
public LCEngineRealtimeHookType HookType {
get;
}
public LCEngineRealtimeHookAttribute(LCEngineRealtimeHookType hookType) {
HookType = hookType;
}
}
}

View File

@ -0,0 +1,19 @@
using System;
namespace LeanCloud.Engine {
public enum LCEngineUserHookType {
SMS,
Email,
OnLogin
}
public class LCEngineUserHookAttribute : Attribute {
public LCEngineUserHookType HookType {
get;
}
public LCEngineUserHookAttribute(LCEngineUserHookType hookType) {
HookType = hookType;
}
}
}

14
Engine/Engine.csproj Normal file
View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Common\Common\Common.csproj" />
<ProjectReference Include="..\Storage\Storage\Storage.csproj" />
</ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
using System.Text.Json;
using Microsoft.AspNetCore.Http;
using LeanCloud.Storage.Internal.Object;
using LeanCloud.Storage;
namespace LeanCloud.Engine {
public class LCClassHookHandler {
private static Dictionary<string, MethodInfo> ClassHooks => LCEngine.ClassHooks;
public static void Hello() {
}
public static async Task<object> HandleClassHook(string className, string hookName, HttpRequest request, JsonElement body) {
LCLogger.Debug($"Hook: {className}#{hookName}");
LCLogger.Debug(body.ToString());
LCEngine.CheckHookKey(request);
string classHookName = GetClassHookName(className, hookName);
if (ClassHooks.TryGetValue(classHookName, out MethodInfo mi)) {
Dictionary<string, object> dict = LCEngine.Decode(body);
LCObjectData objectData = LCObjectData.Decode(dict["object"] as Dictionary<string, object>);
objectData.ClassName = className;
LCObject obj = LCObject.Create(className);
obj.Merge(objectData);
LCUser user = null;
if (dict.TryGetValue("user", out object userObj) &&
userObj != null) {
user = new LCUser();
user.Merge(LCObjectData.Decode(userObj as Dictionary<string, object>));
}
LCClassHookRequest req = new LCClassHookRequest {
Object = obj,
CurrentUser = user
};
LCObject result = await LCEngine.Invoke(mi, req) as LCObject;
if (result != null) {
return LCCloud.Encode(result);
}
}
return default;
}
private static string GetClassHookName(string className, string hookName) {
switch (hookName) {
case "beforeSave":
return $"__before_save_for_{className}";
case "afterSave":
return $"__after_save_for_{className}";
case "beforeUpdate":
return $"__before_update_for_{className}";
case "afterUpdate":
return $"__after_update_for_{className}";
case "beforeDelete":
return $"__before_delete_for_{className}";
case "afterDelete":
return $"__after_delete_for_{className}";
default:
throw new Exception($"Error hook name: {hookName}");
}
}
}
}

View File

@ -0,0 +1,82 @@
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
using System.Text.Json;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using LeanCloud.Storage.Internal.Codec;
using LeanCloud.Storage;
namespace LeanCloud.Engine {
public class LCFunctionHandler {
private static Dictionary<string, MethodInfo> Functions => LCEngine.Functions;
/// <summary>
/// 云函数
/// </summary>
/// <param name="funcName"></param>
/// <param name="request"></param>
/// <param name="body"></param>
/// <returns></returns>
public static async Task<object> HandleRun(string funcName, HttpRequest request, JsonElement body) {
LCLogger.Debug($"Run: {funcName}");
LCLogger.Debug(body.ToString());
if (Functions.TryGetValue(funcName, out MethodInfo mi)) {
LCUser currentUser = null;
if (request.Headers.TryGetValue("x-lc-session", out StringValues session)) {
currentUser = await LCUser.BecomeWithSessionToken(session);
}
LCCloudFunctionRequest req = new LCCloudFunctionRequest {
Meta = new LCCloudFunctionRequestMeta {
RemoteAddress = LCEngine.GetIP(request)
},
Params = LCEngine.Decode(body),
SessionToken = session.ToString(),
User = currentUser
};
object result = await LCEngine.Invoke(mi, req);
if (result != null) {
return new Dictionary<string, object> {
{ "result", result }
};
}
}
return default;
}
/// <summary>
/// RPC
/// </summary>
/// <param name="funcName"></param>
/// <param name="request"></param>
/// <param name="body"></param>
/// <returns></returns>
public static async Task<object> HandleRPC(string funcName, HttpRequest request, JsonElement body) {
LCLogger.Debug($"RPC: {funcName}");
LCLogger.Debug(body.ToString());
if (Functions.TryGetValue(funcName, out MethodInfo mi)) {
LCUser currentUser = null;
if (request.Headers.TryGetValue("x-lc-session", out StringValues session)) {
currentUser = await LCUser.BecomeWithSessionToken(session);
}
LCCloudRPCRequest req = new LCCloudRPCRequest {
Meta = new LCCloudFunctionRequestMeta {
RemoteAddress = LCEngine.GetIP(request)
},
Params = LCDecoder.Decode(LCEngine.Decode(body)),
SessionToken = session.ToString(),
User = currentUser
};
object result = await LCEngine.Invoke(mi, req);
if (result != null) {
return new Dictionary<string, object> {
{ "result", LCCloud.Encode(result) }
};
}
}
return default;
}
}
}

View File

@ -0,0 +1,13 @@
using System.Collections.Generic;
namespace LeanCloud.Engine {
public class LCPingHandler {
public static object HandlePing() {
LCLogger.Debug("Ping ~~~");
return new Dictionary<string, string> {
{ "runtime", "dotnet" },
{ "version", LCApplication.SDKVersion }
};
}
}
}

View File

@ -0,0 +1,66 @@
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
using System.Text.Json;
using Microsoft.AspNetCore.Http;
using LeanCloud.Storage.Internal.Object;
using LeanCloud.Storage;
namespace LeanCloud.Engine {
public class LCUserHookHandler {
private static Dictionary<string, MethodInfo> UserHooks => LCEngine.UserHooks;
public static async Task<object> HandleVerifiedSMS(HttpRequest request, JsonElement body) {
LCLogger.Debug(LCEngine.OnSMSVerified);
LCLogger.Debug(body.ToString());
LCEngine.CheckHookKey(request);
if (UserHooks.TryGetValue(LCEngine.OnSMSVerified, out MethodInfo mi)) {
Dictionary<string, object> dict = LCEngine.Decode(body);
return await Invoke(mi, dict);
}
return default;
}
public static async Task<object> HandleVerifiedEmail(HttpRequest request, JsonElement body) {
LCLogger.Debug(LCEngine.OnEmailVerified);
LCLogger.Debug(body.ToString());
LCEngine.CheckHookKey(request);
if (UserHooks.TryGetValue(LCEngine.OnEmailVerified, out MethodInfo mi)) {
Dictionary<string, object> dict = LCEngine.Decode(body);
return await Invoke(mi, dict);
}
return default;
}
public static async Task<object> HandleLogin(HttpRequest request, JsonElement body) {
LCLogger.Debug(LCEngine.OnLogin);
LCLogger.Debug(body.ToString());
LCEngine.CheckHookKey(request);
if (UserHooks.TryGetValue(LCEngine.OnLogin, out MethodInfo mi)) {
Dictionary<string, object> dict = LCEngine.Decode(body);
return await Invoke(mi, dict);
}
return default;
}
private static async Task<object> Invoke(MethodInfo mi, Dictionary<string, object> dict) {
LCObjectData objectData = LCObjectData.Decode(dict["object"] as Dictionary<string, object>);
objectData.ClassName = "_User";
LCObject obj = LCObject.Create("_User");
obj.Merge(objectData);
LCUserHookRequest req = new LCUserHookRequest {
CurrentUser = obj as LCUser
};
return await LCEngine.Invoke(mi, req) as LCObject;
}
}
}

227
Engine/LCEngine.cs Normal file
View File

@ -0,0 +1,227 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Linq;
using System.Threading.Tasks;
using System.Text.Json;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
using Microsoft.Extensions.Primitives;
using LeanCloud.Common;
namespace LeanCloud.Engine {
public class LCEngine {
const string LCMasterKeyName = "x-avoscloud-master-key";
const string LCHookKeyName = "x-lc-hook-key";
const string BeforeSave = "__before_save_for_";
const string AfterSave = "__after_save_for_";
const string BeforeUpdate = "__before_update_for_";
const string AfterUpdate = "__after_update_for_";
const string BeforeDelete = "__before_delete_for_";
const string AfterDelete = "__after_delete_for_";
internal const string OnSMSVerified = "__on_verified_sms";
internal const string OnEmailVerified = "__on_verified_email";
internal const string OnLogin = "__on_login__User";
const string ClientOnline = "_clientOnline";
const string ClientOffline = "_clientOffline";
const string MessageSent = "_messageSent";
const string MessageReceived = "_messageReceived";
const string ReceiversOffline = "_receiversOffline";
const string MessageUpdate = "_messageUpdate";
const string ConversationStart = "_conversationStart";
const string ConversationStarted = "_conversationStarted";
const string ConversationAdd = "_conversationAdd";
const string ConversationAdded = "_conversationAdded";
const string ConversationRemove = "_conversationRemove";
const string ConversationRemoved = "_conversationRemoved";
const string ConversationUpdate = "_conversationUpdate";
public static Dictionary<string, MethodInfo> Functions = new Dictionary<string, MethodInfo>();
public static Dictionary<string, MethodInfo> ClassHooks = new Dictionary<string, MethodInfo>();
public static Dictionary<string, MethodInfo> UserHooks = new Dictionary<string, MethodInfo>();
public static void Initialize() {
// 获取环境变量
LCLogger.Debug("-------------------------------------------------");
PrintEnvironmentVar("LEANCLOUD_APP_ID");
PrintEnvironmentVar("LEANCLOUD_APP_KEY");
PrintEnvironmentVar("LEANCLOUD_APP_MASTER_KEY");
PrintEnvironmentVar("LEANCLOUD_APP_HOOK_KEY");
PrintEnvironmentVar("LEANCLOUD_API_SERVER");
PrintEnvironmentVar("LEANCLOUD_APP_PROD");
PrintEnvironmentVar("LEANCLOUD_APP_ENV");
PrintEnvironmentVar("LEANCLOUD_APP_INSTANCE");
PrintEnvironmentVar("LEANCLOUD_REGION");
PrintEnvironmentVar("LEANCLOUD_APP_ID");
PrintEnvironmentVar("LEANCLOUD_APP_DOMAIN");
PrintEnvironmentVar("LEANCLOUD_APP_PORT");
LCLogger.Debug("-------------------------------------------------");
LCApplication.Initialize(Environment.GetEnvironmentVariable("LEANCLOUD_APP_ID"),
Environment.GetEnvironmentVariable("LEANCLOUD_APP_KEY"),
Environment.GetEnvironmentVariable("LEANCLOUD_API_SERVER"));
Assembly assembly = Assembly.GetCallingAssembly();
ClassHooks = assembly.GetTypes()
.SelectMany(t => t.GetMethods())
.Where(m => m.GetCustomAttribute<LCEngineObjectHookAttribute>() != null)
.ToDictionary(mi => {
LCEngineObjectHookAttribute attr = mi.GetCustomAttribute<LCEngineObjectHookAttribute>();
switch (attr.HookType) {
case LCEngineObjectHookType.BeforeSave:
return $"{BeforeSave}{attr.ClassName}";
case LCEngineObjectHookType.AfterSave:
return $"{AfterSave}{attr.ClassName}";
case LCEngineObjectHookType.BeforeUpdate:
return $"{BeforeUpdate}{attr.ClassName}";
case LCEngineObjectHookType.AfterUpdate:
return $"{AfterUpdate}{attr.ClassName}";
case LCEngineObjectHookType.BeforeDelete:
return $"{BeforeDelete}{attr.ClassName}";
case LCEngineObjectHookType.AfterDelete:
return $"{AfterDelete}{attr.ClassName}";
default:
throw new Exception($"Error hook type: {attr.HookType}");
}
});
UserHooks = assembly.GetTypes()
.SelectMany(t => t.GetMethods())
.Where(m => m.GetCustomAttribute<LCEngineUserHookAttribute>() != null)
.ToDictionary(mi => {
LCEngineUserHookAttribute attr = mi.GetCustomAttribute<LCEngineUserHookAttribute>();
switch (attr.HookType) {
case LCEngineUserHookType.SMS:
return OnSMSVerified;
case LCEngineUserHookType.Email:
return OnEmailVerified;
case LCEngineUserHookType.OnLogin:
return OnLogin;
default:
throw new Exception($"Error hook type: {attr.HookType}");
}
});
Functions = assembly.GetTypes()
.SelectMany(t => t.GetMethods())
.Where(m => m.GetCustomAttribute<LCEngineFunctionAttribute>() != null)
.ToDictionary(mi => mi.GetCustomAttribute<LCEngineFunctionAttribute>().FunctionName);
assembly.GetTypes()
.SelectMany(t => t.GetMethods())
.Where(m => m.GetCustomAttribute<LCEngineRealtimeHookAttribute>() != null)
.ToDictionary(mi => {
LCEngineRealtimeHookAttribute attr = mi.GetCustomAttribute<LCEngineRealtimeHookAttribute>();
switch (attr.HookType) {
case LCEngineRealtimeHookType.ClientOnline:
return ClientOnline;
case LCEngineRealtimeHookType.ClientOffline:
return ClientOffline;
case LCEngineRealtimeHookType.MessageSent:
return MessageSent;
case LCEngineRealtimeHookType.MessageReceived:
return MessageReceived;
case LCEngineRealtimeHookType.ReceiversOffline:
return ReceiversOffline;
case LCEngineRealtimeHookType.MessageUpdate:
return MessageUpdate;
case LCEngineRealtimeHookType.ConversationStart:
return ConversationStart;
case LCEngineRealtimeHookType.ConversationStarted:
return ConversationStarted;
case LCEngineRealtimeHookType.ConversationAdd:
return ConversationAdd;
case LCEngineRealtimeHookType.ConversationAdded:
return ConversationAdded;
case LCEngineRealtimeHookType.ConversationRemove:
return ConversationRemove;
case LCEngineRealtimeHookType.ConversationRemoved:
return ConversationRemoved;
case LCEngineRealtimeHookType.ConversationUpdate:
return ConversationUpdate;
default:
throw new Exception($"Error hook type: {attr.HookType}");
}
})
.ToList()
.ForEach(item => {
Functions.TryAdd(item.Key, item.Value);
});
}
public static void PrintEnvironmentVar(string key) {
LCLogger.Debug($"{key} : {Environment.GetEnvironmentVariable(key)}");
}
internal static async Task<object> Invoke(MethodInfo mi, object request) {
try {
object[] ps = new object[] { request };
if (mi.ReturnType == typeof(Task) ||
(mi.ReturnType.IsGenericType && mi.ReturnType.GetGenericTypeDefinition() == typeof(Task<>))) {
Task task = mi.Invoke(null, ps) as Task;
await task;
return task.GetType().GetProperty("Result")?.GetValue(task);
}
return mi.Invoke(null, ps);
} catch (TargetInvocationException e) {
throw e.InnerException;
}
}
internal static Dictionary<string, object> Decode(JsonElement jsonElement) {
string json = System.Text.Json.JsonSerializer.Serialize(jsonElement);
Dictionary<string, object> dict = JsonConvert.DeserializeObject<Dictionary<string, object>>(json,
LCJsonConverter.Default);
return dict;
}
internal static string GetIP(HttpRequest request) {
if (request.Headers.TryGetValue("x-real-ip", out StringValues ip)) {
return ip.ToString();
}
if (request.Headers.TryGetValue("x-forwarded-for", out StringValues forward)) {
return forward.ToString();
}
return request.HttpContext.Connection.RemoteIpAddress.ToString();
}
internal static void CheckMasterKey(HttpRequest request) {
if (!request.Headers.TryGetValue(LCMasterKeyName, out StringValues masterKey)) {
throw new Exception("No master key");
}
if (!masterKey.Equals(Environment.GetEnvironmentVariable("LEANCLOUD_APP_MASTER_KEY"))) {
throw new Exception("Mismatch master key");
}
}
internal static void CheckHookKey(HttpRequest request) {
if (!request.Headers.TryGetValue(LCHookKeyName, out StringValues hookKey)) {
throw new Exception("No hook key");
}
if (!hookKey.Equals(Environment.GetEnvironmentVariable("LEANCLOUD_APP_HOOK_KEY"))) {
throw new Exception("Mismatch hook key");
}
}
public static object GetFunctions(HttpRequest request) {
CheckMasterKey(request);
List<string> functions = new List<string>();
functions.AddRange(Functions.Keys);
functions.AddRange(ClassHooks.Keys);
functions.AddRange(UserHooks.Keys);
foreach (string func in functions) {
LCLogger.Debug(func);
}
return new Dictionary<string, List<string>> {
{ "result", functions }
};
}
}
}

View File

@ -0,0 +1,13 @@
using LeanCloud.Storage;
namespace LeanCloud.Engine {
public class LCClassHookRequest {
public LCObject Object {
get; set;
}
public LCUser CurrentUser {
get; set;
}
}
}

View File

@ -0,0 +1,28 @@
using System.Collections.Generic;
using LeanCloud.Storage;
namespace LeanCloud.Engine {
public class LCCloudFunctionRequestMeta {
public string RemoteAddress {
get; set;
}
}
public class LCCloudFunctionRequest {
public LCCloudFunctionRequestMeta Meta {
get; set;
}
public Dictionary<string, object> Params {
get; set;
}
public LCUser User {
get; set;
}
public string SessionToken {
get; set;
}
}
}

View File

@ -0,0 +1,21 @@
using LeanCloud.Storage;
namespace LeanCloud.Engine {
public class LCCloudRPCRequest {
public LCCloudFunctionRequestMeta Meta {
get; set;
}
public object Params {
get; set;
}
public LCUser User {
get; set;
}
public string SessionToken {
get; set;
}
}
}

View File

@ -0,0 +1,9 @@
using LeanCloud.Storage;
namespace LeanCloud.Engine {
public class LCUserHookRequest {
public LCUser CurrentUser {
get; set;
}
}
}