* csharp-sdk.sln: chore: 重构 AVFile 为 AVObject 的子类

* RTM.csproj:
* AVQuery2.cs:
* AVRealtime.cs:
* AVIMNotice.cs:
* AVIMClient.cs:
* AVIMMessage.cs:
* Utils.cs:
* ICacheEngine.cs:
* IAVIMMessage.cs:
* AVIMSignature.cs:
* AVIMEventArgs.cs:
* IAVIMListener.cs:
* AVIMException.cs:
* JustTest.cs:
* FileTest.cs:
* AVIMEnumerator.cs:
* AVFile.cs:
* QueryTest.cs:
* AVIMTextMessage.cs:
* AVQuery.cs:
* AVObject.cs:
* AVIMTypedMessage.cs:
* AVIMImageMessage.cs:
* IAVTimer.cs:
* AVIMConversation.cs:
* AVClient.cs:
* AVIMAudioMessage.cs:
* ISignatureFactory.cs:
* AVIMCorePlugins.cs:
* AVIMBinaryMessage.cs:
* AVIMRecalledMessage.cs:
* AVIMMessageListener.cs:
* AckCommand.cs:
* IAVIMPlatformHooks.cs:
* AVIMConversationQuery.cs:
* AVIMCommand.cs:
* ReadCommand.cs:
* PatchCommand.cs:
* GoAwayListener.cs:
* AVIMProtocol.cs:
* MessageCommand.cs:
* SessionListener.cs:
* SessionCommand.cs:
* AVIMTemporaryConversation.cs:
* RouterState.cs:
* AVRouterController.cs:
* AVIMCommandRunner.cs:
* IAVIMCommandRunner.cs:
* AVDecoder.cs:
* IAVRouterController.cs:
* IWebSocketClient.cs:
* MessagePatchListener.cs:
* ConversationCommand.cs:
* AVIMMessageFieldNameAttribute.cs:
* AVIMMessageClassNameAttribute.cs:
* QueryOperation.cs:
* QueryCondition.cs:
* OfflineMessageListener.cs:
* AVIMTypedMessageTypeIntAttribute.cs:
* AVTimer.Portable.cs:
* AVIMConversationListener.cs:
* ConversationUnreadListener.cs:
* StringEngine.cs:
* AWSUploader.cs:
* DateTimeEngine.cs:
* QiniuUploader.cs:
* QCloudUploader.cs:
* DictionaryEngine.cs:
* AVFileController.cs:
* FreeStyleMessageClassInfo.cs:
* DefaultWebSocketClient.Portable.cs:
* FreeStyleMessageClassingController.cs:
* IFreeStyleMessageClassingController.cs:
oneRain 2019-09-12 15:07:19 +08:00
parent bc5d396ab9
commit 5b9da20d74
74 changed files with 9261 additions and 291 deletions

View File

@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LeanCloud.Realtime.Internal
{
internal class AVIMCorePlugins
{
private static readonly AVIMCorePlugins instance = new AVIMCorePlugins();
public static AVIMCorePlugins Instance
{
get
{
return instance;
}
}
private readonly object mutex = new object();
private IAVRouterController routerController;
public IAVRouterController RouterController
{
get
{
lock (mutex)
{
routerController = routerController ?? new AVRouterController();
return routerController;
}
}
internal set
{
lock (mutex)
{
routerController = value;
}
}
}
private IFreeStyleMessageClassingController freeStyleClassingController;
public IFreeStyleMessageClassingController FreeStyleClassingController
{
get
{
lock (mutex)
{
freeStyleClassingController = freeStyleClassingController ?? new FreeStyleMessageClassingController();
return freeStyleClassingController;
}
}
}
}
}

View File

@ -0,0 +1,176 @@
using LeanCloud;
using LeanCloud.Storage.Internal;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LeanCloud.Realtime.Internal
{
/// <summary>
/// Command.
/// </summary>
public class AVIMCommand
{
protected readonly string cmd;
protected readonly string op;
protected string appId;
protected string peerId;
protected AVIMSignature signature;
protected readonly IDictionary<string, object> arguments;
public int TimeoutInSeconds { get; set; }
protected readonly IDictionary<string, object> estimatedData = new Dictionary<string, object>();
internal readonly object mutex = new object();
internal static readonly object Mutex = new object();
public AVIMCommand() :
this(arguments: new Dictionary<string, object>())
{
}
protected AVIMCommand(string cmd = null,
string op = null,
string appId = null,
string peerId = null,
AVIMSignature signature = null,
IDictionary<string, object> arguments = null)
{
this.cmd = cmd;
this.op = op;
this.arguments = arguments == null ? new Dictionary<string, object>() : arguments;
this.peerId = peerId;
this.signature = signature;
}
protected AVIMCommand(AVIMCommand source,
string cmd = null,
string op = null,
string appId = null,
string peerId = null,
IDictionary<string, object> arguments = null,
AVIMSignature signature = null)
{
if (source == null)
{
throw new ArgumentNullException("source", "Source can not be null");
}
this.cmd = source.cmd;
this.op = source.op;
this.arguments = source.arguments;
this.peerId = source.peerId;
this.appId = source.appId;
this.signature = source.signature;
if (cmd != null)
{
this.cmd = cmd;
}
if (op != null)
{
this.op = op;
}
if (arguments != null)
{
this.arguments = arguments;
}
if (peerId != null)
{
this.peerId = peerId;
}
if (appId != null)
{
this.appId = appId;
}
if (signature != null)
{
this.signature = signature;
}
}
public AVIMCommand Command(string cmd)
{
return new AVIMCommand(this, cmd: cmd);
}
public AVIMCommand Option(string op)
{
return new AVIMCommand(this, op: op);
}
public AVIMCommand Argument(string key, object value)
{
lock (mutex)
{
this.arguments[key] = value;
return new AVIMCommand(this);
}
}
public AVIMCommand AppId(string appId)
{
this.appId = appId;
return new AVIMCommand(this, appId: appId);
}
public AVIMCommand PeerId(string peerId)
{
this.peerId = peerId;
return new AVIMCommand(this, peerId: peerId);
}
public AVIMCommand IDlize()
{
this.Argument("i", AVIMCommand.NextCmdId);
return this;
}
public virtual IDictionary<string, object> Encode()
{
lock (mutex)
{
estimatedData.Clear();
estimatedData.Merge(arguments);
estimatedData.Add("cmd", cmd);
estimatedData.Add("appId", this.appId);
if (!string.IsNullOrEmpty(op))
estimatedData.Add("op", op);
if (!string.IsNullOrEmpty(peerId))
estimatedData.Add("peerId", peerId);
return estimatedData;
}
}
public virtual string EncodeJsonString()
{
var json = this.Encode();
return Json.Encode(json);
}
public bool IsValid
{
get
{
return !string.IsNullOrEmpty(this.cmd);
}
}
private static Int32 lastCmdId = -65536;
internal static Int32 NextCmdId
{
get
{
lock (Mutex)
{
lastCmdId++;
if (lastCmdId > ushort.MaxValue)
{
lastCmdId = -65536;
}
return lastCmdId;
}
}
}
}
}

View File

@ -0,0 +1,92 @@
using LeanCloud;
using LeanCloud.Storage.Internal;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace LeanCloud.Realtime.Internal
{
public class AVIMCommandRunner : IAVIMCommandRunner
{
private readonly IWebSocketClient webSocketClient;
public AVIMCommandRunner(IWebSocketClient webSocketClient)
{
this.webSocketClient = webSocketClient;
}
public void RunCommand(AVIMCommand command)
{
command.IDlize();
var requestString = command.EncodeJsonString();
AVRealtime.PrintLog("websocket=>" + requestString);
webSocketClient.Send(requestString);
}
/// <summary>
///
/// </summary>
/// <param name="command"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public Task<Tuple<int, IDictionary<string, object>>> RunCommandAsync(AVIMCommand command, CancellationToken cancellationToken = default(CancellationToken))
{
var tcs = new TaskCompletionSource<Tuple<int, IDictionary<string, object>>>();
command.IDlize();
var requestString = command.EncodeJsonString();
if (!command.IsValid)
{
requestString = "{}";
}
AVRealtime.PrintLog("websocket=>" + requestString);
webSocketClient.Send(requestString);
var requestJson = command.Encode();
Action<string> onMessage = null;
onMessage = (response) =>
{
//AVRealtime.PrintLog("response<=" + response);
var responseJson = Json.Parse(response) as IDictionary<string, object>;
if (responseJson.Keys.Contains("i"))
{
if (requestJson["i"].ToString() == responseJson["i"].ToString())
{
var result = new Tuple<int, IDictionary<string, object>>(-1, responseJson);
if (responseJson.Keys.Contains("code"))
{
var errorCode = int.Parse(responseJson["code"].ToString());
var reason = string.Empty;
int appCode = 0;
if (responseJson.Keys.Contains("reason"))
{
reason = responseJson["reason"].ToString();
}
if (responseJson.Keys.Contains("appCode"))
{
appCode = int.Parse(responseJson["appCode"].ToString());
}
tcs.SetException(new AVIMException(errorCode, appCode, reason, null));
}
if (tcs.Task.Exception == null)
{
tcs.SetResult(result);
}
webSocketClient.OnMessage -= onMessage;
}
else
{
}
}
};
webSocketClient.OnMessage += onMessage;
return tcs.Task;
}
}
}

View File

@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LeanCloud.Realtime.Internal
{
internal class AckCommand : AVIMCommand
{
public AckCommand()
: base(cmd: "ack")
{
}
public AckCommand(AVIMCommand source)
: base(source)
{
}
public AckCommand Message(IAVIMMessage message)
{
return new AckCommand()
.ConversationId(message.ConversationId)
.MessageId(message.Id);
}
public AckCommand MessageId(string messageId)
{
if (string.IsNullOrEmpty(messageId))
{
messageId = "";
}
return new AckCommand(this.Argument("mid", messageId));
}
public AckCommand ConversationId(string conversationId)
{
if (string.IsNullOrEmpty(conversationId))
{
conversationId = "";
}
return new AckCommand(this.Argument("cid", conversationId));
}
public AckCommand FromTimeStamp(long startTimeStamp)
{
return new AckCommand(this.Argument("fromts", startTimeStamp));
}
public AckCommand ToTimeStamp(long endTimeStamp)
{
return new AckCommand(this.Argument("tots", endTimeStamp));
}
public AckCommand ReadAck()
{
return new AckCommand(this.Argument("read", true));
}
}
}

View File

@ -0,0 +1,129 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LeanCloud.Realtime.Internal
{
internal class ConversationCommand : AVIMCommand
{
protected IList<string> members;
public ConversationCommand()
: base(cmd: "conv")
{
}
public ConversationCommand(AVIMCommand source)
: base(source: source)
{
}
public ConversationCommand Member(string clientId)
{
if (members == null)
{
members = new List<string>();
}
members.Add(clientId);
return Members(members);
}
public ConversationCommand Members(IEnumerable<string> members)
{
this.members = members.ToList();
return new ConversationCommand(this.Argument("m", members));
}
public ConversationCommand Transient(bool isTransient)
{
return new ConversationCommand(this.Argument("transient", isTransient));
}
public ConversationCommand Unique(bool isUnique)
{
return new ConversationCommand(this.Argument("unique", isUnique));
}
public ConversationCommand Temporary(bool isTemporary)
{
return new ConversationCommand(this.Argument("tempConv", isTemporary));
}
public ConversationCommand TempConvTTL(double tempConvTTL)
{
return new ConversationCommand(this.Argument("tempConvTTL", tempConvTTL));
}
public ConversationCommand Attr(IDictionary<string, object> attr)
{
return new ConversationCommand(this.Argument("attr", attr));
}
public ConversationCommand Set(string key, object value)
{
return new ConversationCommand(this.Argument(key, value));
}
public ConversationCommand ConversationId(string conversationId)
{
return new ConversationCommand(this.Argument("cid", conversationId));
}
public ConversationCommand Generate(AVIMConversation conversation)
{
var attr = conversation.EncodeAttributes();
var cmd = new ConversationCommand()
.ConversationId(conversation.ConversationId)
.Attr(attr)
.Members(conversation.MemberIds)
.Transient(conversation.IsTransient)
.Temporary(conversation.IsTemporary);
if (conversation.IsTemporary)
{
var ttl = (conversation.expiredAt.Value - DateTime.Now).TotalSeconds;
cmd = cmd.TempConvTTL(ttl);
}
return cmd;
}
public ConversationCommand Where(object encodedQueryString)
{
return new ConversationCommand(this.Argument("where", encodedQueryString));
}
public ConversationCommand Limit(int limit)
{
return new ConversationCommand(this.Argument("limit", limit));
}
public ConversationCommand Skip(int skip)
{
return new ConversationCommand(this.Argument("skip", skip));
}
public ConversationCommand Count()
{
return new ConversationCommand(this.Argument("count", 1));
}
public ConversationCommand Sort(string sort)
{
return new ConversationCommand(this.Argument("sort", sort));
}
public ConversationCommand TargetClientId(string targetClientId)
{
return new ConversationCommand(this.Argument("targetClientId", targetClientId));
}
public ConversationCommand QueryAllMembers(bool queryAllMembers)
{
return new ConversationCommand(this.Argument("queryAllMembers", queryAllMembers));
}
}
}

View File

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace LeanCloud.Realtime.Internal
{
public interface IAVIMCommandRunner
{
Task<Tuple<int, IDictionary<string, object>>> RunCommandAsync(AVIMCommand command,
CancellationToken cancellationToken = default(CancellationToken));
void RunCommand(AVIMCommand command);
}
}

View File

@ -0,0 +1,83 @@
using LeanCloud.Storage.Internal;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LeanCloud.Realtime.Internal
{
internal class MessageCommand : AVIMCommand
{
public MessageCommand()
: base(cmd: "direct")
{
}
public MessageCommand(AVIMCommand source)
: base(source: source)
{
}
public MessageCommand ConvId(string convId)
{
return new MessageCommand(this.Argument("cid", convId));
}
public MessageCommand Receipt(bool receipt)
{
return new MessageCommand(this.Argument("r", receipt));
}
public MessageCommand Transient(bool transient)
{
if (transient) return new MessageCommand(this.Argument("transient", transient));
return new MessageCommand(this);
}
public MessageCommand Priority(int priority)
{
if (priority > 1) return new MessageCommand(this.Argument("level", priority));
return new MessageCommand(this);
}
public MessageCommand Will(bool will)
{
if (will) return new MessageCommand(this.Argument("will", will));
return new MessageCommand(this);
}
public MessageCommand Distinct(string token)
{
return new MessageCommand(this.Argument("dt", token));
}
public MessageCommand Message(string msg)
{
return new MessageCommand(this.Argument("msg", msg));
}
public MessageCommand BinaryEncode(bool binaryEncode)
{
return new MessageCommand(this.Argument("bin", binaryEncode));
}
public MessageCommand PushData(IDictionary<string, object> pushData)
{
return new MessageCommand(this.Argument("pushData", Json.Encode(pushData)));
}
public MessageCommand Mention(IEnumerable<string> clientIds)
{
var mentionedMembers = clientIds.ToList();
return new MessageCommand(this.Argument("mentionPids", mentionedMembers));
}
public MessageCommand MentionAll(bool mentionAll)
{
return new MessageCommand(this.Argument("mentionAll", mentionAll));
}
public MessageCommand Binary(byte[] data)
{
return new MessageCommand(this.Argument("binaryMsg", data));
}
}
}

View File

@ -0,0 +1,98 @@
using LeanCloud.Storage.Internal;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LeanCloud.Realtime.Internal
{
internal class PatchCommand : AVIMCommand
{
internal struct Patch
{
public string MessageId { get; set; }
public string ConvId { get; set; }
public string From { get; set; }
public long MetaTimestamp { get; set; }
public long PatchTimestamp { get; set; }
public string PatchData { get; set; }
public bool Recall { get; set; }
public byte[] BinaryData { get; set; }
public bool MentionAll { get; set; }
public IEnumerable<string> MentionIds { get; set; }
public IDictionary<string, object> Encode()
{
return new Dictionary<string, object>()
{
{ "cid",this.ConvId},
{ "mid",this.MessageId},
{ "from",this.From},
{ "timestamp",this.MetaTimestamp},
{ "recall",this.Recall},
{ "data",this.PatchData},
{ "patchTimestamp",this.PatchTimestamp},
{ "binaryMsg",this.BinaryData},
{ "mentionAll",this.MentionAll},
{ "meintonPids",this.MentionIds}
} as IDictionary<string, object>;
}
}
public PatchCommand()
: base(cmd: "patch", op: "modify")
{
this.Patches = new List<Patch>();
}
public PatchCommand(AVIMCommand source, ICollection<Patch> sourcePatchs)
: base(source: source)
{
this.Patches = sourcePatchs;
}
public ICollection<Patch> Patches { get; set; }
public IList<IDictionary<string, object>> EncodePatches()
{
return this.Patches.Select(p => p.Encode().Trim()).ToList();
}
public PatchCommand Recall(IAVIMMessage message)
{
var patch = new Patch()
{
ConvId = message.ConversationId,
From = message.FromClientId,
MessageId = message.Id,
MetaTimestamp = message.ServerTimestamp,
Recall = true,
PatchTimestamp = DateTime.Now.ToUnixTimeStamp()
};
this.Patches.Add(patch);
this.Argument("patches", this.EncodePatches());
return new PatchCommand(this, this.Patches);
}
public PatchCommand Modify(IAVIMMessage oldMessage, IAVIMMessage newMessage)
{
var patch = new Patch()
{
ConvId = oldMessage.ConversationId,
From = oldMessage.FromClientId,
MessageId = oldMessage.Id,
MetaTimestamp = oldMessage.ServerTimestamp,
Recall = false,
PatchTimestamp = DateTime.Now.ToUnixTimeStamp(),
PatchData = newMessage.Serialize()
};
this.Patches.Add(patch);
this.Argument("patches", this.EncodePatches());
return new PatchCommand(this, this.Patches);
}
}
}

View File

@ -0,0 +1,91 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LeanCloud.Realtime.Internal
{
internal class ReadCommand : AVIMCommand
{
internal class ConvRead
{
internal string ConvId { get; set; }
internal string MessageId { get; set; }
internal long Timestamp { get; set; }
public override bool Equals(object obj)
{
ConvRead cr = obj as ConvRead;
return cr.ConvId == this.ConvId;
}
public override int GetHashCode()
{
return this.ConvId.GetHashCode() ^ this.MessageId.GetHashCode() ^ this.Timestamp.GetHashCode();
}
}
public ReadCommand()
: base(cmd: "read")
{
}
public ReadCommand(AVIMCommand source)
: base(source)
{
}
public ReadCommand ConvId(string convId)
{
return new ReadCommand(this.Argument("cid", convId));
}
public ReadCommand ConvIds(IEnumerable<string> convIds)
{
if (convIds != null)
{
if (convIds.Count() > 0)
{
return new ReadCommand(this.Argument("cids", convIds.ToList()));
}
}
return this;
}
public ReadCommand Conv(ConvRead conv)
{
return Convs(new ConvRead[] { conv });
}
public ReadCommand Convs(IEnumerable<ConvRead> convReads)
{
if (convReads != null)
{
if (convReads.Count() > 0)
{
IList<IDictionary<string, object>> payload = new List<IDictionary<string, object>>();
foreach (var convRead in convReads)
{
var convDic = new Dictionary<string, object>();
convDic.Add("cid", convRead.ConvId);
if (!string.IsNullOrEmpty(convRead.MessageId))
{
convDic.Add("mid", convRead.MessageId);
}
if (convRead.Timestamp != 0)
{
convDic.Add("timestamp", convRead.Timestamp);
}
payload.Add(convDic);
}
return new ReadCommand(this.Argument("convs", payload));
}
}
return this;
}
}
}

View File

@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LeanCloud.Realtime.Internal
{
internal class SessionCommand : AVIMCommand
{
static readonly int MESSAGE_RECALL_AND_MODIFY = 0x1;
public SessionCommand()
: base(cmd: "session")
{
arguments.Add("configBitmap", MESSAGE_RECALL_AND_MODIFY);
}
public SessionCommand(AVIMCommand source)
:base(source: source)
{
}
public SessionCommand UA(string ua)
{
return new SessionCommand(this.Argument("ua", ua));
}
public SessionCommand Tag(string tag)
{
if (string.IsNullOrEmpty(tag)) return new SessionCommand(this);
return new SessionCommand(this.Argument("tag", tag));
}
public SessionCommand DeviceId(string deviceId)
{
if (string.IsNullOrEmpty(deviceId)) return new SessionCommand(this);
return new SessionCommand(this.Argument("deviceId", deviceId));
}
public SessionCommand R(int r)
{
return new SessionCommand(this.Argument("r", r));
}
public SessionCommand SessionToken(string st)
{
return new SessionCommand(this.Argument("st", st));
}
public SessionCommand SessionPeerIds(IEnumerable<string> sessionPeerIds)
{
return new SessionCommand(this.Argument("sessionPeerIds", sessionPeerIds.ToList()));
}
}
}

View File

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LeanCloud.Realtime.Internal
{
internal enum UnixTimeStampUnit
{
Second = 1,
Milisecond = 1000,
}
internal static class DateTimeEngine
{
public static long ToUnixTimeStamp(this DateTime date, UnixTimeStampUnit unit = UnixTimeStampUnit.Milisecond)
{
long unixTimestamp = (long)(date.ToUniversalTime().Subtract(new DateTime(1970, 1, 1))).TotalSeconds;
return (unixTimestamp * (int)unit);
}
public static DateTime ToDateTime(this long timestamp, UnixTimeStampUnit unit = UnixTimeStampUnit.Milisecond)
{
var timespan = timestamp * 1000 / (int)(unit);
DateTime dtDateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
dtDateTime = dtDateTime.AddMilliseconds(timespan).ToLocalTime();
return dtDateTime;
}
}
}

View File

@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LeanCloud.Realtime.Internal
{
internal static class DictionaryEngine
{
internal static IDictionary<string, object> Merge(this IDictionary<string, object> dataLeft, IDictionary<string, object> dataRight)
{
if (dataRight == null)
return dataLeft;
foreach (var kv in dataRight)
{
if (dataLeft.ContainsKey(kv.Key))
{
dataLeft[kv.Key] = kv.Value;
}
else
{
dataLeft.Add(kv);
}
}
return dataLeft;
}
internal static object Grab(this IDictionary<string, object> data, string path)
{
var keys = path.Split('.').ToList<string>();
if (keys.Count == 1) return data[keys[0]];
var deep = data[keys[0]] as IDictionary<string, object>;
keys.RemoveAt(0);
string deepPath = string.Join(".", keys.ToArray());
return Grab(deep, deepPath);
}
internal static IDictionary<TKey, TValue> Trim<TKey, TValue>(this IDictionary<TKey, TValue> data)
{
return data.Where(kvp => kvp.Value != null).ToDictionary(k => k.Key, v => v.Value);
}
}
}

View File

@ -0,0 +1,36 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Text;
namespace LeanCloud.Realtime.Internal
{
internal static class StringEngine
{
internal static string Random(this string str, int length)
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz";
var random = new Random();
return new string(Enumerable.Repeat(chars, length)
.Select(s => s[random.Next(s.Length)]).ToArray());
}
internal static string TempConvId<T>(this IEnumerable<T> objs)
{
var orderedBase64Strs = objs.Select(obj => Encoding.UTF8.ToBase64(obj.ToString())).OrderBy(a => a, StringComparer.Ordinal).ToArray();
return "_tmp:" + string.Join("$", orderedBase64Strs);
}
internal static string ToBase64(this System.Text.Encoding encoding, string text)
{
if (text == null)
{
return null;
}
byte[] textAsBytes = encoding.GetBytes(text);
return Convert.ToBase64String(textAsBytes);
}
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LeanCloud.Realtime.Internal
{
interface IAVIMPlatformHooks
{
IWebSocketClient WebSocketClient { get; }
string ua { get; }
}
}

View File

@ -0,0 +1,75 @@
using LeanCloud.Storage.Internal;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
namespace LeanCloud.Realtime.Internal
{
internal class FreeStyleMessageClassInfo
{
public TypeInfo TypeInfo { get; private set; }
public IDictionary<String, String> PropertyMappings { get; private set; }
private ConstructorInfo Constructor { get; set; }
//private MethodInfo ValidateMethod { get; set; }
public int TypeInt { get; set; }
public FreeStyleMessageClassInfo(Type type, ConstructorInfo constructor)
{
TypeInfo = type.GetTypeInfo();
Constructor = constructor;
PropertyMappings = ReflectionHelpers.GetProperties(type)
.Select(prop => Tuple.Create(prop, prop.GetCustomAttribute<AVIMMessageFieldNameAttribute>(true)))
.Where(t => t.Item2 != null)
.Select(t => Tuple.Create(t.Item1, t.Item2.FieldName))
.ToDictionary(t => t.Item1.Name, t => t.Item2);
}
public bool Validate(string msgStr)
{
var instance = Instantiate(msgStr);
if (instance is AVIMTypedMessage)
{
try
{
var msgDic = Json.Parse(msgStr) as IDictionary<string, object>;
if (msgDic != null)
{
if (msgDic.ContainsKey(AVIMProtocol.LCTYPE))
{
return msgDic[AVIMProtocol.LCTYPE].ToString() == TypeInt.ToString();
}
}
}
catch (Exception ex)
{
if (ex is ArgumentException)
{
return instance.Validate(msgStr);
}
}
}
return instance.Validate(msgStr);
}
public IAVIMMessage Instantiate(string msgStr)
{
var rtn = (IAVIMMessage)Constructor.Invoke(null);
return rtn;
}
public static string GetMessageClassName(TypeInfo type)
{
var attribute = type.GetCustomAttribute<AVIMMessageClassNameAttribute>();
return attribute != null ? attribute.ClassName : null;
}
public static int GetTypedInteger(TypeInfo type)
{
var attribute = type.GetCustomAttribute<AVIMTypedMessageTypeIntAttribute>();
return attribute != null ? attribute.TypeInteger : 0;
}
}
}

View File

@ -0,0 +1,210 @@
using LeanCloud.Storage.Internal;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
namespace LeanCloud.Realtime.Internal
{
internal class FreeStyleMessageClassingController : IFreeStyleMessageClassingController
{
private static readonly string messageClassName = "_AVIMMessage";
private readonly IDictionary<string, FreeStyleMessageClassInfo> registeredInterfaces;
private readonly ReaderWriterLockSlim mutex;
public FreeStyleMessageClassingController()
{
mutex = new ReaderWriterLockSlim();
registeredInterfaces = new Dictionary<string, FreeStyleMessageClassInfo>();
}
public Type GetType(IDictionary<string, object> msg)
{
throw new NotImplementedException();
}
public IAVIMMessage Instantiate(string msgStr, IDictionary<string, object> buildInData)
{
FreeStyleMessageClassInfo info = null;
mutex.EnterReadLock();
bool bin = false;
if (buildInData.ContainsKey("bin"))
{
bool.TryParse(buildInData["bin"].ToString(), out bin);
}
if (bin)
{
var binMessage = new AVIMBinaryMessage();
this.DecodeProperties(binMessage, buildInData);
return binMessage;
}
var reverse = registeredInterfaces.Values.Reverse();
foreach (var subInterface in reverse)
{
if (subInterface.Validate(msgStr))
{
info = subInterface;
break;
}
}
mutex.ExitReadLock();
var message = info != null ? info.Instantiate(msgStr) : new AVIMMessage();
this.DecodeProperties(message, buildInData);
message.Deserialize(msgStr);
return message;
}
public IAVIMMessage DecodeProperties(IAVIMMessage message, IDictionary<string, object> buildInData)
{
long timestamp;
if (buildInData.ContainsKey("timestamp"))
{
if (long.TryParse(buildInData["timestamp"].ToString(), out timestamp))
{
message.ServerTimestamp = timestamp;
}
}
long ackAt;
if (buildInData.ContainsKey("ackAt"))
{
if (long.TryParse(buildInData["ackAt"].ToString(), out ackAt))
{
message.RcpTimestamp = ackAt;
}
}
if (buildInData.ContainsKey("from"))
{
message.FromClientId = buildInData["from"].ToString();
}
if (buildInData.ContainsKey("msgId"))
{
message.Id = buildInData["msgId"].ToString();
}
if (buildInData.ContainsKey("cid"))
{
message.ConversationId = buildInData["cid"].ToString();
}
if (buildInData.ContainsKey("fromPeerId"))
{
message.FromClientId = buildInData["fromPeerId"].ToString();
}
if (buildInData.ContainsKey("id"))
{
message.Id = buildInData["id"].ToString();
}
if (buildInData.ContainsKey("mid"))
{
message.Id = buildInData["mid"].ToString();
}
if (buildInData.ContainsKey("mentionPids"))
{
message.MentionList = AVDecoder.Instance.DecodeList<string>(buildInData["mentionPids"]);
}
if (buildInData.TryGetValue("patchTimestamp", out object patchTimestampObj)) {
if (long.TryParse(patchTimestampObj.ToString(), out long patchTimestamp)) {
message.UpdatedAt = patchTimestamp;
}
}
bool mentionAll;
if (buildInData.ContainsKey("mentionAll"))
{
if (bool.TryParse(buildInData["mentionAll"].ToString(), out mentionAll))
{
message.MentionAll = mentionAll;
}
}
return message;
}
public IDictionary<string, object> EncodeProperties(IAVIMMessage subclass)
{
var type = subclass.GetType();
var result = new Dictionary<string, object>();
var className = GetClassName(type);
var typeInt = GetTypeInt(type);
var propertMappings = GetPropertyMappings(className);
foreach (var propertyPair in propertMappings)
{
var propertyInfo = ReflectionHelpers.GetProperty(type, propertyPair.Key);
var operation = propertyInfo.GetValue(subclass, null);
if (operation != null)
result[propertyPair.Value] = PointerOrLocalIdEncoder.Instance.Encode(operation);
}
if (typeInt != 0)
{
result[AVIMProtocol.LCTYPE] = typeInt;
}
return result;
}
public bool IsTypeValid(IDictionary<string, object> msg, Type type)
{
return true;
}
public void RegisterSubclass(Type type)
{
TypeInfo typeInfo = type.GetTypeInfo();
if (!typeof(IAVIMMessage).GetTypeInfo().IsAssignableFrom(typeInfo))
{
throw new ArgumentException("Cannot register a type that is not a implementation of IAVIMMessage");
}
var className = GetClassName(type);
var typeInt = GetTypeInt(type);
try
{
mutex.EnterWriteLock();
ConstructorInfo constructor = type.FindConstructor();
if (constructor == null)
{
throw new ArgumentException("Cannot register a type that does not implement the default constructor!");
}
var classInfo = new FreeStyleMessageClassInfo(type, constructor);
if (typeInt != 0)
{
classInfo.TypeInt = typeInt;
}
registeredInterfaces[className] = classInfo;
}
finally
{
mutex.ExitWriteLock();
}
}
public String GetClassName(Type type)
{
return type == typeof(IAVIMMessage)
? messageClassName
: FreeStyleMessageClassInfo.GetMessageClassName(type.GetTypeInfo());
}
public int GetTypeInt(Type type)
{
return type == typeof(AVIMTypedMessage) ? 0 : FreeStyleMessageClassInfo.GetTypedInteger(type.GetTypeInfo());
}
public IDictionary<String, String> GetPropertyMappings(String className)
{
FreeStyleMessageClassInfo info = null;
mutex.EnterReadLock();
registeredInterfaces.TryGetValue(className, out info);
if (info == null)
{
registeredInterfaces.TryGetValue(messageClassName, out info);
}
mutex.ExitReadLock();
return info.PropertyMappings;
}
}
}

View File

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace LeanCloud.Realtime.Internal
{
interface IFreeStyleMessageClassingController
{
bool IsTypeValid(IDictionary<string,object> msg, Type type);
void RegisterSubclass(Type t);
IAVIMMessage Instantiate(string msgStr,IDictionary<string,object> buildInData);
IDictionary<string, object> EncodeProperties(IAVIMMessage subclass);
Type GetType(IDictionary<string, object> msg);
String GetClassName(Type type);
IDictionary<String, String> GetPropertyMappings(String className);
}
}

View File

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LeanCloud.Realtime.Internal
{
internal class AVIMProtocol
{
#region msg format
static internal readonly string LCTYPE = "_lctype";
static internal readonly string LCFILE = "_lcfile";
static internal readonly string LCTEXT = "_lctext";
static internal readonly string LCATTRS = "_lcattrs";
static internal readonly string LCLOC = "_lcloc";
#endregion
}
}

View File

@ -0,0 +1,173 @@
using LeanCloud.Storage.Internal;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace LeanCloud.Realtime.Internal
{
internal class AVRouterController : IAVRouterController
{
const string routerUrl = "http://router.g0.push.leancloud.cn/v1/route?appId={0}";
const string routerKey = "LeanCloud_RouterState";
public Task<PushRouterState> GetAsync(string pushRouter = null, bool secure = true, CancellationToken cancellationToken = default(CancellationToken))
{
//return Task.FromResult(new PushRouterState()
//{
// server = "wss://rtm57.leancloud.cn/"
//});
return LoadAysnc(cancellationToken).OnSuccess(_ =>
{
var cache = _.Result;
var task = Task.FromResult<PushRouterState>(cache);
if (cache == null || cache.expire < DateTime.Now.ToUnixTimeStamp())
{
task = QueryAsync(pushRouter, secure, cancellationToken);
}
return task;
}).Unwrap();
}
/// <summary>
/// 清理地址缓存
/// </summary>
/// <returns>The cache.</returns>
public Task ClearCache() {
var tcs = new TaskCompletionSource<bool>();
AVPlugins.Instance.StorageController.LoadAsync().ContinueWith(t => {
if (t.IsFaulted) {
tcs.SetResult(true);
} else {
var storage = t.Result;
if (storage.ContainsKey(routerKey)) {
storage.RemoveAsync(routerKey).ContinueWith(_ => tcs.SetResult(true));
} else {
tcs.SetResult(true);
}
}
});
return tcs.Task;
}
Task<PushRouterState> LoadAysnc(CancellationToken cancellationToken)
{
try
{
return AVPlugins.Instance.StorageController.LoadAsync().OnSuccess(_ =>
{
var currentCache = _.Result;
object routeCacheStr = null;
if (currentCache.TryGetValue(routerKey, out routeCacheStr))
{
var routeCache = routeCacheStr as IDictionary<string, object>;
var routerState = new PushRouterState()
{
groupId = routeCache["groupId"] as string,
server = routeCache["server"] as string,
secondary = routeCache["secondary"] as string,
ttl = long.Parse(routeCache["ttl"].ToString()),
expire = long.Parse(routeCache["expire"].ToString()),
source = "localCache"
};
return routerState;
}
return null;
});
}
catch
{
return Task.FromResult<PushRouterState>(null);
}
}
Task<PushRouterState> QueryAsync(string pushRouter, bool secure, CancellationToken cancellationToken)
{
var routerHost = pushRouter;
if (routerHost == null) {
var appRouter = AVPlugins.Instance.AppRouterController.Get();
routerHost = string.Format("https://{0}/v1/route?appId={1}", appRouter.RealtimeRouterServer, AVClient.CurrentConfiguration.ApplicationId) ?? appRouter.RealtimeRouterServer ?? string.Format(routerUrl, AVClient.CurrentConfiguration.ApplicationId);
}
AVRealtime.PrintLog($"router: {routerHost}");
AVRealtime.PrintLog($"push: {pushRouter}");
if (!string.IsNullOrEmpty(pushRouter))
{
var rtmUri = new Uri(pushRouter);
if (!string.IsNullOrEmpty(rtmUri.Scheme))
{
var url = new Uri(rtmUri, "v1/route").ToString();
routerHost = string.Format("{0}?appId={1}", url, AVClient.CurrentConfiguration.ApplicationId);
}
else
{
routerHost = string.Format("https://{0}/v1/route?appId={1}", pushRouter, AVClient.CurrentConfiguration.ApplicationId);
}
}
if (secure)
{
routerHost += "&secure=1";
}
AVRealtime.PrintLog("use push router url:" + routerHost);
return AVClient.RequestAsync(uri: new Uri(routerHost),
method: "GET",
headers: null,
data: null,
contentType: "application/json",
cancellationToken: CancellationToken.None).ContinueWith<PushRouterState>(t =>
{
if (t.Exception != null)
{
var innnerException = t.Exception.InnerException;
AVRealtime.PrintLog(innnerException.Message);
throw innnerException;
}
var httpStatus = (int)t.Result.Item1;
if (httpStatus != 200)
{
return null;
}
try
{
var result = t.Result.Item2;
var routerState = Json.Parse(result) as IDictionary<string, object>;
if (routerState.Keys.Count == 0)
{
throw new KeyNotFoundException("Can not get websocket url from server,please check the appId.");
}
var ttl = long.Parse(routerState["ttl"].ToString());
var expire = DateTime.Now.AddSeconds(ttl);
routerState["expire"] = expire.ToUnixTimeStamp();
//save to local cache async.
AVPlugins.Instance.StorageController.LoadAsync().OnSuccess(storage => storage.Result.AddAsync(routerKey, routerState));
var routerStateObj = new PushRouterState()
{
groupId = routerState["groupId"] as string,
server = routerState["server"] as string,
secondary = routerState["secondary"] as string,
ttl = long.Parse(routerState["ttl"].ToString()),
expire = expire.ToUnixTimeStamp(),
source = "online"
};
return routerStateObj;
}
catch (Exception e)
{
if (e is KeyNotFoundException)
{
throw e;
}
return null;
}
});
}
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace LeanCloud.Realtime.Internal
{
interface IAVRouterController
{
Task<PushRouterState> GetAsync(string pushRouter, bool secure, CancellationToken cancellationToken = default(CancellationToken));
Task ClearCache();
}
}

View File

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LeanCloud.Realtime.Internal
{
internal class PushRouterState
{
public string groupId { get; internal set; }
public string server { get; internal set; }
public long ttl { get; internal set; }
public long expire { get; internal set; }
public string secondary { get; internal set; }
public string groupUrl { get; internal set; }
public string source { get; internal set; }
}
}

View File

@ -0,0 +1,49 @@
using System;
namespace LeanCloud.Realtime.Internal
{
public interface IAVTimer
{
/// <summary>
/// Start this timer.
/// </summary>
void Start();
/// <summary>
/// Stop this timer.
/// </summary>
void Stop();
bool Enabled { get; set; }
/// <summary>
/// The number of milliseconds between timer events.
/// </summary>
/// <value>The interval.</value>
double Interval { get; set; }
/// <summary>
/// 已经执行了多少次
/// </summary>
long Executed { get; }
/// <summary>
/// Occurs when elapsed.
/// </summary>
event EventHandler<TimerEventArgs> Elapsed;
}
/// <summary>
/// Timer event arguments.
/// </summary>
public class TimerEventArgs : EventArgs
{
public TimerEventArgs(DateTime signalTime)
{
SignalTime = signalTime;
}
public DateTime SignalTime
{
get;
private set;
}
}
}

View File

@ -0,0 +1,107 @@
using System;
using System.Threading.Tasks;
using System.Threading;
namespace LeanCloud.Realtime.Internal
{
internal delegate void TimerCallback();
internal sealed class Timer : CancellationTokenSource, IDisposable
{
TimerCallback exe;
int Interval { get; set; }
internal Timer(TimerCallback callback, int interval, bool enable)
{
exe = callback;
Interval = interval;
Enabled = enable;
Execute();
}
Task Execute()
{
if (Enabled)
return Task.Delay(Interval).ContinueWith(t =>
{
if (!Enabled)
return null;
exe();
return this.Execute();
});
else
return Task.FromResult(0);
}
volatile bool enabled;
public bool Enabled
{
get {
return enabled;
} set {
enabled = value;
}
}
}
public class AVTimer : IAVTimer
{
public AVTimer()
{
}
Timer timer;
public bool Enabled
{
get
{
return timer.Enabled;
}
set
{
timer.Enabled = value;
}
}
public double Interval
{
get; set;
}
long executed;
public long Executed
{
get
{
return executed;
}
internal set
{
executed = value;
}
}
public void Start()
{
if (timer == null)
{
timer = new Timer(() =>
{
Elapsed(this, new TimerEventArgs(DateTime.Now));
}, (int)Interval, true);
}
}
public void Stop()
{
if (timer != null) timer.Enabled = false;
}
public event EventHandler<TimerEventArgs> Elapsed;
}
}

View File

@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace LeanCloud.Realtime.Internal
{
/// <summary>
/// LeanCloud WebSocket 客户端接口
/// </summary>
public interface IWebSocketClient {
/// <summary>
/// 客户端 WebSocket 长连接是否打开
/// </summary>
bool IsOpen { get; }
/// <summary>
/// WebSocket 长连接关闭时触发的事件回调
/// </summary>
event Action<int, string, string> OnClosed;
/// <summary>
/// 云端发送数据包给客户端WebSocket 接受到时触发的事件回调
/// </summary>
event Action<string> OnMessage;
/// <summary>
/// 客户端 WebSocket 长连接成功打开时,触发的事件回调
/// </summary>
event Action OnOpened;
/// <summary>
/// 主动关闭连接
/// </summary>
void Close();
void Disconnect();
/// <summary>
/// 打开连接
/// </summary>
/// <param name="url">wss 地址</param>
/// <param name="protocol">子协议</param>
void Open(string url, string protocol = null);
/// <summary>
/// 发送数据包的接口
/// </summary>
/// <param name="message"></param>
void Send(string message);
Task<bool> Connect(string url, string protocol = null);
}
}

View File

@ -0,0 +1,107 @@
using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Net.WebSockets;
namespace LeanCloud.Realtime.Internal {
/// <summary>
/// LeanCloud Realtime SDK for .NET Portable 内置默认的 WebSocketClient
/// </summary>
public class DefaultWebSocketClient {
const int RECV_BUFFER_SIZE = 1024;
ClientWebSocket client;
public event Action<int, string> OnClose;
public event Action OnOpened;
public event Action<string> OnMessage;
public bool IsOpen {
get {
return client != null && client.State == WebSocketState.Open;
}
}
public async Task Close() {
if (IsOpen) {
await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "1", CancellationToken.None);
}
}
public void Disconnect() {
OnClose?.Invoke(0, string.Empty);
_ = Close();
}
public async Task Send(string message) {
if (!IsOpen) {
throw new Exception("WebSocket is not open when send data.");
}
ArraySegment<byte> bytes = new ArraySegment<byte>(Encoding.UTF8.GetBytes(message));
try {
await client.SendAsync(bytes, WebSocketMessageType.Text, true, default);
} catch (InvalidOperationException e) {
OnClose?.Invoke(-2, e.Message);
_ = Close();
throw e;
}
}
public async Task Connect(string url, string protocol = null) {
client = new ClientWebSocket();
client.Options.AddSubProtocol(protocol);
client.Options.KeepAliveInterval = TimeSpan.FromSeconds(10);
try {
await client.ConnectAsync(new Uri(url), default);
// 开始接收
_ = StartReceive();
} catch (Exception e) {
throw e;
}
}
async Task StartReceive() {
byte[] buffer = new byte[RECV_BUFFER_SIZE];
try {
while (client.State == WebSocketState.Open) {
byte[] data = new byte[0];
WebSocketReceiveResult result;
do {
result = await client.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Close) {
// 断开事件
AVRealtime.PrintLog($"-------------------- WebSocket Close: {result.CloseStatus}, {result.CloseStatusDescription}");
OnClose?.Invoke((int)result.CloseStatus, result.CloseStatusDescription);
return;
}
data = await MergeData(data, buffer, result.Count);
} while (!result.EndOfMessage);
// 一个 WebSocket 消息体接收完成
try {
string message = Encoding.UTF8.GetString(data);
OnMessage(message);
} catch (Exception e) {
AVRealtime.PrintLog($"************************* Parse command error: {e.Message}");
}
}
} catch (Exception e) {
AVRealtime.PrintLog($"-------------------- WebSocket Receive Exception: {e.Message}");
AVRealtime.PrintLog(e.StackTrace);
// 断线事件
OnClose?.Invoke(-1, e.Message);
}
}
static async Task<byte[]> MergeData(byte[] oldData, byte[] newData, int newDataLength) {
return await Task.Run(() => {
var data = new byte[oldData.Length + newDataLength];
Array.Copy(oldData, data, oldData.Length);
Array.Copy(newData, 0, data, oldData.Length, newDataLength);
AVRealtime.PrintLog($"merge: {data.Length}");
return data;
});
}
}
}

View File

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LeanCloud.Realtime
{
/// <summary>
/// Audio message.
/// </summary>
[AVIMMessageClassName("_AVIMAudioMessage")]
[AVIMTypedMessageTypeInt(-3)]
public class AVIMAudioMessage : AVIMFileMessage
{
}
/// <summary>
/// Video message.
/// </summary>
[AVIMMessageClassName("_AVIMVideoMessage")]
[AVIMTypedMessageTypeInt(-4)]
public class AVIMVideoMessage: AVIMFileMessage
{
}
}

View File

@ -0,0 +1,42 @@
using System;
using LeanCloud.Realtime.Internal;
namespace LeanCloud.Realtime
{
/// <summary>
/// 基于二进制数据的消息类型,可以直接发送 Byte 数组
/// </summary>
[AVIMMessageClassName("_AVIMBinaryMessage")]
public class AVIMBinaryMessage : AVIMMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="T:LeanCloud.Realtime.AVIMBinaryMessage"/> class.
/// </summary>
public AVIMBinaryMessage()
{
}
/// <summary>
/// create new instance of AVIMBinnaryMessage
/// </summary>
/// <param name="data"></param>
public AVIMBinaryMessage(byte[] data)
{
this.BinaryData = data;
}
/// <summary>
/// Gets or sets the binary data.
/// </summary>
/// <value>The binary data.</value>
public byte[] BinaryData { get; set; }
internal override MessageCommand BeforeSend(MessageCommand cmd)
{
var result = base.BeforeSend(cmd);
result = result.Binary(this.BinaryData);
return result;
}
}
}

1195
RTM/RTM/Public/AVIMClient.cs Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,180 @@
using LeanCloud.Realtime.Internal;
using LeanCloud.Storage.Internal;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace LeanCloud.Realtime
{
/// <summary>
/// 对话查询类
/// </summary>
public class AVIMConversationQuery : AVQuery<AVObject>
{
internal AVIMClient CurrentClient { get; set; }
internal AVIMConversationQuery(AVIMClient _currentClient)
: base()
{
CurrentClient = _currentClient;
}
bool compact;
bool withLastMessageRefreshed;
private AVIMConversationQuery(AVIMConversationQuery source,
IDictionary<string, object> where = null,
IEnumerable<string> replacementOrderBy = null,
IEnumerable<string> thenBy = null,
int? skip = null,
int? limit = null,
IEnumerable<string> includes = null,
IEnumerable<string> selectedKeys = null,
string redirectClassNameForKey = null)
: base(source, where, replacementOrderBy, thenBy, skip, limit, includes, selectedKeys, redirectClassNameForKey)
{
}
/// <summary>
/// Creates the instance.
/// </summary>
/// <returns>The instance.</returns>
/// <param name="where">Where.</param>
/// <param name="replacementOrderBy">Replacement order by.</param>
/// <param name="thenBy">Then by.</param>
/// <param name="skip">Skip.</param>
/// <param name="limit">Limit.</param>
/// <param name="includes">Includes.</param>
/// <param name="selectedKeys">Selected keys.</param>
/// <param name="redirectClassNameForKey">Redirect class name for key.</param>
public AVIMConversationQuery CreateInstance(
IDictionary<string, object> where = null,
IEnumerable<string> replacementOrderBy = null,
IEnumerable<string> thenBy = null,
int? skip = null,
int? limit = null,
IEnumerable<string> includes = null,
IEnumerable<string> selectedKeys = null,
String redirectClassNameForKey = null)
{
var rtn = new AVIMConversationQuery(this, where, replacementOrderBy, thenBy, skip, limit, includes);
rtn.CurrentClient = this.CurrentClient;
rtn.compact = this.compact;
rtn.withLastMessageRefreshed = this.withLastMessageRefreshed;
return rtn;
}
/// <summary>
/// Withs the last message refreshed.
/// </summary>
/// <returns>The last message refreshed.</returns>
/// <param name="enabled">If set to <c>true</c> enabled.</param>
public AVIMConversationQuery WithLastMessageRefreshed(bool enabled)
{
this.withLastMessageRefreshed = enabled;
return this;
}
public AVIMConversationQuery Compact(bool enabled)
{
this.compact = enabled;
return this;
}
internal ConversationCommand GenerateQueryCommand()
{
var cmd = new ConversationCommand();
var queryParameters = this.BuildParameters(false);
if (queryParameters != null)
{
if (queryParameters.Keys.Contains("where"))
cmd.Where(queryParameters["where"]);
if (queryParameters.Keys.Contains("skip"))
cmd.Skip(int.Parse(queryParameters["skip"].ToString()));
if (queryParameters.Keys.Contains("limit"))
cmd.Limit(int.Parse(queryParameters["limit"].ToString()));
if (queryParameters.Keys.Contains("sort"))
cmd.Sort(queryParameters["order"].ToString());
}
return cmd;
}
public override Task<int> CountAsync(CancellationToken cancellationToken = default) {
var convCmd = GenerateQueryCommand();
convCmd.Count();
convCmd.Limit(0);
var cmd = convCmd.Option("query");
return CurrentClient.RunCommandAsync(convCmd).OnSuccess(t =>
{
var result = t.Result.Item2;
if (result.ContainsKey("count"))
{
return int.Parse(result["count"].ToString());
}
return 0;
});
}
/// <summary>
/// 查找符合条件的对话
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public override Task<IEnumerable<AVIMConversation>> FindAsync(CancellationToken cancellationToken = default)
{
var convCmd = this.GenerateQueryCommand().Option("query");
return CurrentClient.RunCommandAsync(convCmd).OnSuccess(t =>
{
var result = t.Result.Item2;
IList<AVIMConversation> rtn = new List<AVIMConversation>();
var conList = result["results"] as IList<object>;
if (conList != null)
{
foreach (var c in conList)
{
var cData = c as IDictionary<string, object>;
if (cData != null)
{
var con = AVIMConversation.CreateWithData(cData, CurrentClient);
rtn.Add(con);
}
}
}
return rtn.AsEnumerable();
});
}
public override Task<AVIMConversation> FirstAsync(CancellationToken cancellationToken = default)
{
return this.FirstOrDefaultAsync();
}
public override Task<AVIMConversation> FirstOrDefaultAsync(CancellationToken cancellationToken = default)
{
var firstQuery = this.Limit(1);
return firstQuery.FindAsync().OnSuccess(t =>
{
return t.Result.FirstOrDefault();
});
}
public override Task<AVIMConversation> GetAsync(string objectId, CancellationToken cancellationToken = default)
{
var idQuery = this.WhereEqualTo("objectId", objectId);
return idQuery.FirstAsync();
}
}
}

View File

@ -0,0 +1,248 @@
using LeanCloud.Storage.Internal;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using LeanCloud.Realtime.Internal;
namespace LeanCloud.Realtime
{
/// <summary>
/// AVIMM essage pager.
/// </summary>
public class AVIMMessagePager
{
/// <summary>
/// Gets the query.
/// </summary>
/// <value>The query.</value>
public AVIMMessageQuery Query { get; private set; }
/// <summary>
/// Gets the size of the page.
/// </summary>
/// <value>The size of the page.</value>
public int PageSize
{
get
{
return Query.Limit;
}
private set
{
Query.Limit = value;
}
}
/// <summary>
/// Gets the current message identifier flag.
/// </summary>
/// <value>The current message identifier flag.</value>
public string CurrentMessageIdFlag
{
get
{
return Query.StartMessageId;
}
private set
{
Query.StartMessageId = value;
}
}
/// <summary>
/// Gets the current date time flag.
/// </summary>
/// <value>The current date time flag.</value>
public DateTime CurrentDateTimeFlag
{
get
{
return Query.From;
}
private set
{
Query.From = value;
}
}
internal AVIMMessagePager()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="T:LeanCloud.Realtime.AVIMMessagePager"/> class.
/// </summary>
/// <param name="conversation">Conversation.</param>
public AVIMMessagePager(AVIMConversation conversation)
: this()
{
Query = conversation.GetMessageQuery();
PageSize = 20;
CurrentDateTimeFlag = DateTime.Now;
}
/// <summary>
/// Sets the size of the page.
/// </summary>
/// <returns>The page size.</returns>
/// <param name="pageSize">Page size.</param>
public AVIMMessagePager SetPageSize(int pageSize)
{
PageSize = pageSize;
return this;
}
/// <summary>
/// Previouses the async.
/// </summary>
/// <returns>The async.</returns>
public Task<IEnumerable<IAVIMMessage>> PreviousAsync()
{
return Query.FindAsync().OnSuccess(t =>
{
var headerMessage = t.Result.FirstOrDefault();
if (headerMessage != null)
{
CurrentMessageIdFlag = headerMessage.Id;
CurrentDateTimeFlag = headerMessage.ServerTimestamp.ToDateTime();
}
return t.Result;
});
}
/// <summary>
/// from previous to lastest.
/// </summary>
/// <returns></returns>
public Task<IEnumerable<IAVIMMessage>> NextAsync()
{
return Query.ReverseFindAsync().OnSuccess(t =>
{
var tailMessage = t.Result.LastOrDefault();
if (tailMessage != null)
{
CurrentMessageIdFlag = tailMessage.Id;
CurrentDateTimeFlag = tailMessage.ServerTimestamp.ToDateTime();
}
return t.Result;
});
}
}
/// <summary>
/// history message interator.
/// </summary>
public class AVIMMessageQuery
{
/// <summary>
/// Gets or sets the convsersation.
/// </summary>
/// <value>The convsersation.</value>
public AVIMConversation Convsersation { get; set; }
/// <summary>
/// Gets or sets the limit.
/// </summary>
/// <value>The limit.</value>
public int Limit { get; set; }
/// <summary>
/// Gets or sets from.
/// </summary>
/// <value>From.</value>
public DateTime From { get; set; }
/// <summary>
/// Gets or sets to.
/// </summary>
/// <value>To.</value>
public DateTime To { get; set; }
/// <summary>
/// Gets or sets the end message identifier.
/// </summary>
/// <value>The end message identifier.</value>
public string EndMessageId { get; set; }
/// <summary>
/// Gets or sets the start message identifier.
/// </summary>
/// <value>The start message identifier.</value>
public string StartMessageId { get; set; }
internal AVIMMessageQuery()
{
Limit = 20;
From = DateTime.Now;
}
/// <summary>
/// Initializes a new instance of the <see cref="T:LeanCloud.Realtime.AVIMMessageQuery"/> class.
/// </summary>
/// <param name="conversation">Conversation.</param>
public AVIMMessageQuery(AVIMConversation conversation)
: this()
{
Convsersation = conversation;
}
/// <summary>
/// Sets the limit.
/// </summary>
/// <returns>The limit.</returns>
/// <param name="limit">Limit.</param>
public AVIMMessageQuery SetLimit(int limit)
{
Limit = limit;
return this;
}
/// <summary>
/// from lastest to previous.
/// </summary>
/// <returns></returns>
public Task<IEnumerable<IAVIMMessage>> FindAsync()
{
return FindAsync<IAVIMMessage>();
}
/// <summary>
/// from lastest to previous.
/// </summary>
/// <returns></returns>
public Task<IEnumerable<IAVIMMessage>> ReverseFindAsync()
{
return ReverseFindAsync<IAVIMMessage>();
}
/// <summary>
/// Finds the async.
/// </summary>
/// <returns>The async.</returns>
/// <param name="reverse">set direction to reverse,it means query direct is from old to new.</param>
/// <typeparam name="T">The 1st type parameter.</typeparam>
public Task<IEnumerable<T>> FindAsync<T>(bool reverse = false)
where T : IAVIMMessage
{
return Convsersation.QueryMessageAsync<T>(
beforeTimeStampPoint: From,
afterTimeStampPoint: To,
limit: Limit,
afterMessageId: EndMessageId,
beforeMessageId: StartMessageId,
direction: reverse ? 0 : 1);
}
/// <summary>
/// from previous to lastest.
/// </summary>
/// <returns></returns>
public Task<IEnumerable<T>> ReverseFindAsync<T>()
where T : IAVIMMessage
{
return FindAsync<T>(true);
}
}
}

View File

@ -0,0 +1,251 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace LeanCloud.Realtime
{
/// <summary>
///
/// </summary>
public class AVIMEventArgs : EventArgs
{
public AVIMEventArgs()
{
}
public AVIMException.ErrorCode ErrorCode { get; internal set; }
/// <summary>
/// LeanCloud 服务端发往客户端消息通知
/// </summary>
public string Message { get; set; }
}
public class AVIMDisconnectEventArgs : EventArgs
{
public int Code { get; private set; }
public string Reason { get; private set; }
public string Detail { get; private set; }
public AVIMDisconnectEventArgs()
{
}
public AVIMDisconnectEventArgs(int _code, string _reason, string _detail)
{
this.Code = _code;
this.Reason = _reason;
this.Detail = _detail;
}
}
/// <summary>
/// 开始重连之后触发正在重连的事件通知,提供给监听者的事件参数
/// </summary>
public class AVIMReconnectingEventArgs : EventArgs
{
/// <summary>
/// 是否由 SDK 内部机制启动的自动重连
/// </summary>
public bool IsAuto { get; set; }
/// <summary>
/// 重连的 client Id
/// </summary>
public string ClientId { get; set; }
/// <summary>
/// 重连时使用的 SessionToken
/// </summary>
public string SessionToken { get; set; }
}
/// <summary>
/// 重连成功之后的事件回调
/// </summary>
public class AVIMReconnectedEventArgs : EventArgs
{
/// <summary>
/// 是否由 SDK 内部机制启动的自动重连
/// </summary>
public bool IsAuto { get; set; }
/// <summary>
/// 重连的 client Id
/// </summary>
public string ClientId { get; set; }
/// <summary>
/// 重连时使用的 SessionToken
/// </summary>
public string SessionToken { get; set; }
}
/// <summary>
/// 重连失败之后的事件回调参数
/// </summary>
public class AVIMReconnectFailedArgs : EventArgs
{
/// <summary>
/// 是否由 SDK 内部机制启动的自动重连
/// </summary>
public bool IsAuto { get; set; }
/// <summary>
/// 重连的 client Id
/// </summary>
public string ClientId { get; set; }
/// <summary>
/// 重连时使用的 SessionToken
/// </summary>
public string SessionToken { get; set; }
/// <summary>
/// 失败的原因
/// 0. 客户端网络断开
/// 1. sessionToken 错误或者失效,需要重新创建 client
/// </summary>
public int FailedCode { get; set; }
}
/// <summary>
/// AVIMM essage event arguments.
/// </summary>
public class AVIMMessageEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="T:LeanCloud.Realtime.AVIMMessageEventArgs"/> class.
/// </summary>
/// <param name="iMessage">I message.</param>
public AVIMMessageEventArgs(IAVIMMessage iMessage)
{
Message = iMessage;
}
/// <summary>
/// Gets or sets the message.
/// </summary>
/// <value>The message.</value>
public IAVIMMessage Message { get; internal set; }
}
/// <summary>
/// AVIMMessage event arguments.
/// </summary>
public class AVIMMessagePatchEventArgs : EventArgs
{
public AVIMMessagePatchEventArgs(IAVIMMessage message)
{
Message = message;
}
/// <summary>
/// Gets or sets the message.
/// </summary>
/// <value>The message.</value>
public IAVIMMessage Message { get; internal set; }
}
/// <summary>
/// AVIMT ext message event arguments.
/// </summary>
public class AVIMTextMessageEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="T:LeanCloud.Realtime.AVIMTextMessageEventArgs"/> class.
/// </summary>
/// <param name="raw">Raw.</param>
public AVIMTextMessageEventArgs(AVIMTextMessage raw)
{
TextMessage = raw;
}
/// <summary>
/// Gets or sets the text message.
/// </summary>
/// <value>The text message.</value>
public AVIMTextMessage TextMessage { get; internal set; }
}
/// <summary>
/// 当对话中有人加入时,触发 <seealso cref="AVIMMembersJoinListener.OnMembersJoined"/> 时所携带的事件参数
/// </summary>
public class AVIMOnMembersJoinedEventArgs : EventArgs
{
/// <summary>
/// 加入到对话的 Client Id(s)
/// </summary>
public IEnumerable<string> JoinedMembers { get; internal set; }
/// <summary>
/// 邀请的操作人
/// </summary>
public string InvitedBy { get; internal set; }
/// <summary>
/// 此次操作针对的对话 Id
/// </summary>
public string ConversationId { get; internal set; }
}
/// <summary>
/// 当对话中有人加入时,触发 AVIMMembersJoinListener<seealso cref="AVIMMembersLeftListener.OnMembersLeft"/> 时所携带的事件参数
/// </summary>
public class AVIMOnMembersLeftEventArgs : EventArgs
{
/// <summary>
/// 离开对话的 Client Id(s)
/// </summary>
public IEnumerable<string> LeftMembers { get; internal set; }
/// <summary>
/// 踢出的操作人
/// </summary>
public string KickedBy { get; internal set; }
/// <summary>
/// 此次操作针对的对话 Id
/// </summary>
public string ConversationId { get; internal set; }
}
/// <summary>
/// 当前用户被邀请加入到对话
/// </summary>
public class AVIMOnInvitedEventArgs : EventArgs
{
/// <summary>
/// 邀请的操作人
/// </summary>
public string InvitedBy { get; internal set; }
/// <summary>
/// 此次操作针对的对话 Id
/// </summary>
public string ConversationId { get; internal set; }
}
/// <summary>
/// 当前用户被他人从对话中踢出
/// </summary>
public class AVIMOnKickedEventArgs : EventArgs
{
/// <summary>
/// 踢出的操作人
/// </summary>
public string KickedBy { get; internal set; }
/// <summary>
/// 此次操作针对的对话 Id
/// </summary>
public string ConversationId { get; internal set; }
}
public class AVIMSessionClosedEventArgs : EventArgs
{
public int Code { get; internal set; }
public string Reason { get; internal set; }
public string Detail { get; internal set; }
}
}

View File

@ -0,0 +1,235 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LeanCloud.Realtime
{
/// <summary>
/// 实时通信的异常
/// </summary>
public class AVIMException : Exception
{
/// <summary>
/// 错误代码
/// </summary>
public enum ErrorCode
{
/// <summary>
/// Error code indicating that an unknown error or an error unrelated to LeanCloud
/// occurred.
/// </summary>
OtherCause = -1,
/// <summary>
/// 服务端错误
/// </summary>
FromServer = 4000,
/// <summary>
/// websocket 连接非正常关闭通常见于路由器配置对长连接限制的情况。SDK 会自动重连,无需人工干预。
/// </summary>
UnknownError = 1006,
/// <summary>
/// 应用不存在或应用禁用了实时通信服务
/// </summary>
APP_NOT_AVAILABLE = 4100,
/// <summary>
/// 登录签名验证失败
/// </summary>
SIGNATURE_FAILED = 4102,
/// <summary>
/// Client ClientId 格式错误,超过 64 个字符。
/// </summary>
INVALID_LOGIN = 4103,
/// <summary>
/// Session 没有打开就发送消息,或执行其他操作。常见的错误场景是调用 open session 后直接发送消息,正确的用法是在 Session 打开的回调里执行。
/// </summary>
SESSION_REQUIRED = 4105,
/// <summary>
/// 读超时服务器端长时间没有收到客户端的数据切断连接。SDK 包装了心跳包的机制出现此错误通常是网络问题。SDK 会自动重连,无需人工干预。
/// </summary>
READ_TIMEOUT = 4107,
/// <summary>
/// 登录超时,连接后长时间没有完成 session open。通常是登录被拒绝等原因出现此问题可能是使用方式有误可以 创建工单,由我们技术顾问来给出建议。
/// </summary>
LOGIN_TIMEOUT = 4108,
/// <summary>
/// 包过长。消息大小超过 5 KB请缩短消息或者拆分消息。
/// </summary>
FRAME_TOO_LONG = 4109,
/// <summary>
/// 设置安全域名后,当前登录的域名与安全域名不符合。
/// </summary>
INVALID_ORIGIN = 4110,
/// <summary>
/// 设置单设备登录后,客户端被其他设备挤下线。
/// </summary>
SESSION_CONFLICT = 4111,
/// <summary>
/// 应用容量超限,当天登录用户数已经超过应用设定的最大值。
/// </summary>
APP_QUOTA_EXCEEDED = 4113,
/// <summary>
/// 客户端发送的序列化数据服务器端无法解析。
/// </summary>
UNPARSEABLE_RAW_MESSAGE = 4114,
/// <summary>
/// 客户端被 REST API 管理接口强制下线。
/// </summary>
KICKED_BY_APP = 4115,
/// <summary>
/// 应用单位时间内发送消息量超过限制,消息被拒绝。
/// </summary>
MESSAGE_SENT_QUOTA_EXCEEDED = 4116,
/// <summary>
/// 服务器内部错误,如果反复出现请收集相关线索并 创建工单,我们会尽快解决。
/// </summary>
INTERNAL_ERROR = 4200,
/// <summary>
/// 通过 API 发送消息超时
/// </summary>
SEND_MESSAGE_TIMEOUT = 4201,
/// <summary>
/// 上游 API 调用异常,请查看报错信息了解错误详情
/// </summary>
CONVERSATION_API_FAILED = 4301,
/// <summary>
/// 对话相关操作签名错误
/// </summary>
CONVERSATION_SIGNATURE_FAILED = 4302,
/// <summary>
/// 发送消息,或邀请等操作对应的对话不存在。
/// </summary>
CONVERSATION_NOT_FOUND = 4303,
/// <summary>
/// 对话成员已满,不能再添加。
/// </summary>
CONVERSATION_FULL = 4304,
/// <summary>
/// 对话操作被应用的云引擎 Hook 拒绝
/// </summary>
CONVERSATION_REJECTED_BY_APP = 4305,
/// <summary>
/// 更新对话操作失败
/// </summary>
CONVERSATION_UPDATE_FAILED = 4306,
/// <summary>
/// 该对话为只读,不能更新或增删成员。
/// </summary>
CONVERSATION_READ_ONLY = 4307,
/// <summary>
/// 该对话禁止当前用户发送消息
/// </summary>
CONVERSATION_NOT_ALLOWED = 4308,
/// <summary>
/// 更新对话的请求被拒绝,当前用户不在对话中
/// </summary>
CONVERSATION_UPDATE_REJECT = 4309,
/// <summary>
/// 查询对话失败,常见于慢查询导致的超时或受其他慢查询导致的数据库响应慢
/// </summary>
CONVERSATION_QUERY_FAILED = 4310,
/// <summary>
/// 拉取对话消息记录失败,常见与超时的情况
/// </summary>
CONVERSATION_LOG_FAILED = 4311,
/// <summary>
/// 拉去对话消息记录被拒绝,当前用户不再对话中
/// </summary>
CONVERSATION_LOG_REJECT = 4312,
/// <summary>
/// 该功能仅对系统对话有效
/// </summary>
SYSTEM_CONVERSATION_REQUIRED = 4313,
/// <summary>
/// 该功能仅对普通对话有效。
/// </summary>
NORMAL_CONVERSATION_REQUIRED = 4314,
/// <summary>
/// 当前用户被加入此对话的黑名单,无法发送消息。
/// </summary>
CONVERSATION_BLACKLISTED = 4315,
/// <summary>
/// 该功能仅对暂态对话有效。
/// </summary>
TRANSIENT_CONVERSATION_REQUIRED = 4316,
/// <summary>
/// 发送消息的对话不存在,或当前用户不在对话中
/// </summary>
INVALID_MESSAGING_TARGET = 4401,
/// <summary>
/// 发送的消息被应用的云引擎 Hook 拒绝
/// </summary>
MESSAGE_REJECTED_BY_APP = 4402,
/// <summary>
/// 客户端无法通过 WebSocket 发送数据包
/// </summary>
CAN_NOT_EXCUTE_COMMAND = 1002,
}
/// <summary>
/// 用户云代码返回的错误码
/// </summary>
public int AppCode { get; private set; }
internal AVIMException(ErrorCode code, string message, Exception cause = null)
: base(message, cause)
{
this.Code = code;
}
internal AVIMException(int code, int appCode, string message, Exception cause = null)
: this((ErrorCode)code, message, cause)
{
this.AppCode = appCode;
}
/// <summary>
/// The LeanCloud error code associated with the exception.
/// </summary>
public ErrorCode Code { get; private set; }
}
}

View File

@ -0,0 +1,246 @@
using LeanCloud;
using LeanCloud.Storage.Internal;
using LeanCloud.Realtime.Internal;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
namespace LeanCloud.Realtime
{
/// <summary>
/// 图像消息
/// </summary>
[AVIMMessageClassName("_AVIMImageMessage")]
[AVIMTypedMessageTypeInt(-2)]
public class AVIMImageMessage : AVIMFileMessage
{
}
/// <summary>
/// File message.
/// </summary>
[AVIMMessageClassName("_AVIMFileMessage")]
[AVIMTypedMessageTypeInt(-6)]
public class AVIMFileMessage : AVIMMessageDecorator
{
/// <summary>
/// Initializes a new instance of the <see cref="T:LeanCloud.Realtime.AVIMFileMessage"/> class.
/// </summary>
public AVIMFileMessage()
: base(new AVIMTypedMessage())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="T:LeanCloud.Realtime.AVIMFileMessage"/> class.
/// </summary>
/// <param name="message">Message.</param>
public AVIMFileMessage(AVIMTypedMessage message)
: base(message)
{
}
/// <summary>
/// Gets or sets the file.
/// </summary>
/// <value>The file.</value>
public AVFile File { get; set; }
/// <summary>
/// Froms the URL.
/// </summary>
/// <returns>The URL.</returns>
/// <param name="url">URL.</param>
/// <typeparam name="T">The 1st type parameter.</typeparam>
public static T FromUrl<T>(string url) where T : AVIMFileMessage, new()
{
return FromUrl<T>(string.Empty.Random(8), url, null);
}
/// <summary>
/// From the URL.
/// </summary>
/// <returns>The URL.</returns>
/// <param name="fileName">File name.</param>
/// <param name="externalUrl">External URL.</param>
/// <param name="textTitle">Text title.</param>
/// <param name="customAttributes">Custom attributes.</param>
/// <typeparam name="T">The 1st type parameter.</typeparam>
public static T FromUrl<T>(string fileName, string externalUrl, string textTitle, IDictionary<string, object> customAttributes = null) where T : AVIMFileMessage, new()
{
T message = new T();
message.File = new AVFile(fileName, externalUrl);
message.TextContent = textTitle;
message.MergeCustomAttributes(customAttributes);
return message;
}
/// <summary>
/// From the stream.
/// </summary>
/// <returns>The stream.</returns>
/// <param name="fileName">File name.</param>
/// <param name="data">Data.</param>
/// <param name="mimeType">MIME type.</param>
/// <param name="textTitle">Text title.</param>
/// <param name="metaData">Meta data.</param>
/// <param name="customAttributes">Custom attributes.</param>
/// <typeparam name="T">The 1st type parameter.</typeparam>
public static T FromStream<T>(string fileName, Stream data, string mimeType, string textTitle, IDictionary<string, object> metaData, IDictionary<string, object> customAttributes = null) where T : AVIMFileMessage, new()
{
T message = new T();
message.File = new AVFile(fileName, data, mimeType, metaData);
message.TextContent = textTitle;
message.MergeCustomAttributes(customAttributes);
return message;
}
/// <summary>
/// Encodes the decorator.
/// </summary>
/// <returns>The decorator.</returns>
public override IDictionary<string, object> EncodeDecorator()
{
if (File.Url == null) throw new InvalidOperationException("File.Url can not be null before it can be sent.");
File.MetaData["name"] = File.Name;
File.MetaData["format"] = File.MimeType;
var fileData = new Dictionary<string, object>
{
{ "url", File.Url.ToString()},
{ "objId", File.ObjectId },
{ "metaData", File.MetaData }
};
return new Dictionary<string, object>
{
{ AVIMProtocol.LCFILE, fileData }
};
}
/// <summary>
/// Deserialize the specified msgStr.
/// </summary>
/// <returns>The deserialize.</returns>
/// <param name="msgStr">Message string.</param>
public override IAVIMMessage Deserialize(string msgStr)
{
var msg = Json.Parse(msgStr) as IDictionary<string, object>;
var fileData = msg[AVIMProtocol.LCFILE] as IDictionary<string, object>;
string mimeType = null;
string url = null;
string name = null;
string objId = null;
IDictionary<string, object> metaData = null;
if (fileData != null)
{
object metaDataObj = null;
if (fileData.TryGetValue("metaData", out metaDataObj))
{
metaData = metaDataObj as IDictionary<string, object>;
object fileNameObj = null;
if (metaData != null)
{
if (metaData.TryGetValue("name", out fileNameObj))
{
name = fileNameObj.ToString();
}
}
object mimeTypeObj = null;
if (metaData != null)
{
if (metaData.TryGetValue("format", out mimeTypeObj))
{
if (mimeTypeObj != null)
mimeType = mimeTypeObj.ToString();
}
}
}
object objIdObj = null;
if (fileData.TryGetValue("objId", out objIdObj))
{
if (objIdObj != null)
objId = objIdObj.ToString();
}
object urlObj = null;
if (fileData.TryGetValue("url", out urlObj))
{
url = urlObj.ToString();
}
File = AVFile.CreateWithData(objId, name, url, metaData);
}
return base.Deserialize(msgStr);
}
}
/// <summary>
/// Location message.
/// </summary>
[AVIMMessageClassName("_AVIMMessageClassName")]
[AVIMTypedMessageTypeInt(-5)]
public class AVIMLocationMessage : AVIMMessageDecorator
{
/// <summary>
/// Initializes a new instance of the <see cref="T:LeanCloud.Realtime.AVIMLocationMessage"/> class.
/// </summary>
public AVIMLocationMessage()
: base(new AVIMTypedMessage())
{
}
/// <summary>
/// Gets or sets the location.
/// </summary>
/// <value>The location.</value>
public AVGeoPoint Location { get; set; }
public AVIMLocationMessage(AVGeoPoint location)
: this()
{
Location = location;
}
/// <summary>
/// Encodes the decorator.
/// </summary>
/// <returns>The decorator.</returns>
public override IDictionary<string, object> EncodeDecorator()
{
var locationData = new Dictionary<string, object>
{
{ "longitude", Location.Longitude},
{ "latitude", Location.Latitude }
};
return new Dictionary<string, object>
{
{ AVIMProtocol.LCLOC, locationData }
};
}
/// <summary>
/// Deserialize the specified msgStr.
/// </summary>
/// <returns>The deserialize.</returns>
/// <param name="msgStr">Message string.</param>
public override IAVIMMessage Deserialize(string msgStr)
{
var msg = Json.Parse(msgStr) as IDictionary<string, object>;
var locationData = msg[AVIMProtocol.LCLOC] as IDictionary<string, object>;
if (locationData != null)
{
Location = new AVGeoPoint(double.Parse(locationData["latitude"].ToString()), double.Parse(locationData["longitude"].ToString()));
}
return base.Deserialize(msgStr);
}
}
}

View File

@ -0,0 +1,162 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using LeanCloud;
using System.Reflection;
using LeanCloud.Storage.Internal;
using System.Threading;
using System.Collections;
using LeanCloud.Realtime.Internal;
namespace LeanCloud.Realtime
{
/// <summary>
/// 实时消息的核心基类,它是 Json schema 消息的父类
/// </summary>
[AVIMMessageClassName("_AVIMMessage")]
public class AVIMMessage : IAVIMMessage
{
/// <summary>
/// 默认的构造函数
/// </summary>
public AVIMMessage()
{
}
internal readonly object mutex = new object();
/// <summary>
/// 对话的Id
/// </summary>
public string ConversationId { get; set; }
/// <summary>
/// 发送消息的 ClientId
/// </summary>
public string FromClientId { get; set; }
/// <summary>
/// 消息在全局的唯一标识Id
/// </summary>
public string Id { get; set; }
/// <summary>
/// 服务器端的时间戳
/// </summary>
public long ServerTimestamp { get; set; }
/// <summary>
/// Gets or sets the content.
/// </summary>
/// <value>The content.</value>
public string Content { get; set; }
/// <summary>
/// 对方收到消息的时间戳,如果是多人聊天,那以最早收到消息的人回发的 ACK 为准
/// </summary>
public long RcpTimestamp { get; set; }
public long UpdatedAt { get; set; }
internal string cmdId { get; set; }
#region
/// <summary>
/// Gets or sets a value indicating whether this <see cref="T:LeanCloud.Realtime.IAVIMMessage"/> mention all.
/// </summary>
/// <value><c>true</c> if mention all; otherwise, <c>false</c>.</value>
public bool MentionAll { get; set; }
/// <summary>
/// Gets or sets the mention list.
/// </summary>
/// <value>The mention list.</value>
public IEnumerable<string> MentionList { get; set; }
#endregion
#region register convertor for custom typed message
/// <summary>
/// Serialize this message.
/// </summary>
/// <returns>The serialize.</returns>
public virtual string Serialize()
{
return Content;
}
/// <summary>
/// Validate the specified msgStr.
/// </summary>
/// <returns>The validate.</returns>
/// <param name="msgStr">Message string.</param>
public virtual bool Validate(string msgStr)
{
return true;
}
/// <summary>
/// Deserialize the specified msgStr to message subclass instance
/// </summary>
/// <returns>The deserialize.</returns>
/// <param name="msgStr">Message string.</param>
public virtual IAVIMMessage Deserialize(string msgStr)
{
Content = msgStr;
return this;
}
internal virtual MessageCommand BeforeSend(MessageCommand cmd)
{
return cmd;
}
internal static IAVIMMessage CopyMetaData(IAVIMMessage srcMsg, IAVIMMessage desMsg) {
if (srcMsg == null)
return desMsg;
desMsg.ConversationId = srcMsg.ConversationId;
desMsg.FromClientId = srcMsg.FromClientId;
desMsg.Id = srcMsg.Id;
desMsg.ServerTimestamp = srcMsg.ServerTimestamp;
desMsg.RcpTimestamp = srcMsg.RcpTimestamp;
desMsg.UpdatedAt = srcMsg.UpdatedAt;
return desMsg;
}
#endregion
}
/// <summary>
/// 消息的发送选项
/// </summary>
public struct AVIMSendOptions
{
/// <summary>
/// 是否需要送达回执
/// </summary>
public bool Receipt;
/// <summary>
/// 是否是暂态消息,暂态消息不返回送达回执(ack),不保留离线消息,不触发离线推送
/// </summary>
public bool Transient;
/// <summary>
/// 消息的优先级默认是1可选值还有 2|3
/// </summary>
public int Priority;
/// <summary>
/// 是否为 Will 类型的消息,这条消息会被缓存在服务端,一旦当前客户端下线,这条消息会被发送到对话内的其他成员
/// </summary>
public bool Will;
/// <summary>
/// 如果消息的接收者已经下线了,这个字段的内容就会被离线推送到接收者
///<remarks>例如,一张图片消息的离线消息内容可以类似于:[您收到一条图片消息,点击查看] 这样的推送内容,参照微信的做法</remarks>
/// </summary>
public IDictionary<string, object> PushData;
}
}

View File

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LeanCloud.Realtime
{
[AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)]
public sealed class AVIMMessageClassNameAttribute: Attribute
{
public AVIMMessageClassNameAttribute(string className)
{
this.ClassName = className;
}
public string ClassName { get; private set; }
}
}

View File

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace LeanCloud.Realtime
{
[AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
public sealed class AVIMMessageFieldNameAttribute: Attribute
{
public AVIMMessageFieldNameAttribute(string fieldName)
{
FieldName = fieldName;
}
public string FieldName { get; private set; }
}
}

View File

@ -0,0 +1,143 @@
using LeanCloud.Realtime.Internal;
using LeanCloud.Storage.Internal;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LeanCloud.Realtime
{
/// <summary>
/// 默认的消息监听器,它主要承担的指责是回执的发送与用户自定义的监听器不冲突
/// </summary>
public class AVIMMessageListener : IAVIMListener
{
/// <summary>
/// 默认的 AVIMMessageListener 只会监听 direct 协议,但是并不会触发针对消息类型的判断的监听器
/// </summary>
public AVIMMessageListener()
{
}
/// <summary>
/// Protocols the hook.
/// </summary>
/// <returns><c>true</c>, if hook was protocoled, <c>false</c> otherwise.</returns>
/// <param name="notice">Notice.</param>
public virtual bool ProtocolHook(AVIMNotice notice)
{
if (notice.CommandName != "direct") return false;
if (notice.RawData.ContainsKey("offline")) return false;
return true;
}
private EventHandler<AVIMMessageEventArgs> m_OnMessageReceived;
/// <summary>
/// 接收到聊天消息的事件通知
/// </summary>
public event EventHandler<AVIMMessageEventArgs> OnMessageReceived
{
add
{
m_OnMessageReceived += value;
}
remove
{
m_OnMessageReceived -= value;
}
}
internal virtual void OnMessage(AVIMNotice notice)
{
if (m_OnMessageReceived != null)
{
var msgStr = notice.RawData["msg"].ToString();
var iMessage = AVRealtime.FreeStyleMessageClassingController.Instantiate(msgStr, notice.RawData);
//var messageNotice = new AVIMMessageNotice(notice.RawData);
//var messaegObj = AVIMMessage.Create(messageNotice);
var args = new AVIMMessageEventArgs(iMessage);
m_OnMessageReceived.Invoke(this, args);
}
}
/// <summary>
/// Ons the notice received.
/// </summary>
/// <param name="notice">Notice.</param>
public virtual void OnNoticeReceived(AVIMNotice notice)
{
this.OnMessage(notice);
}
}
/// <summary>
/// 文本消息监听器
/// </summary>
public class AVIMTextMessageListener : IAVIMListener
{
/// <summary>
/// 构建默认的文本消息监听器
/// </summary>
public AVIMTextMessageListener()
{
}
/// <summary>
/// 构建文本消息监听者
/// </summary>
/// <param name="textMessageReceived"></param>
public AVIMTextMessageListener(Action<AVIMTextMessage> textMessageReceived)
{
OnTextMessageReceived += (sender, textMessage) =>
{
textMessageReceived(textMessage.TextMessage);
};
}
private EventHandler<AVIMTextMessageEventArgs> m_OnTextMessageReceived;
public event EventHandler<AVIMTextMessageEventArgs> OnTextMessageReceived
{
add
{
m_OnTextMessageReceived += value;
}
remove
{
m_OnTextMessageReceived -= value;
}
}
public virtual bool ProtocolHook(AVIMNotice notice)
{
if (notice.CommandName != "direct") return false;
try
{
var msg = Json.Parse(notice.RawData["msg"].ToString()) as IDictionary<string, object>;
if (!msg.Keys.Contains(AVIMProtocol.LCTYPE)) return false;
var typInt = 0;
int.TryParse(msg[AVIMProtocol.LCTYPE].ToString(), out typInt);
if (typInt != -1) return false;
return true;
}
catch(ArgumentException)
{
}
return false;
}
public virtual void OnNoticeReceived(AVIMNotice notice)
{
if (m_OnTextMessageReceived != null)
{
var textMessage = new AVIMTextMessage();
textMessage.Deserialize(notice.RawData["msg"].ToString());
m_OnTextMessageReceived(this, new AVIMTextMessageEventArgs(textMessage));
}
}
}
}

View File

@ -0,0 +1,57 @@
using LeanCloud;
using LeanCloud.Realtime.Internal;
using LeanCloud.Storage.Internal;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace LeanCloud.Realtime
{
/// <summary>
///
/// </summary>
public interface IAVIMNotice
{
/// <summary>
///
/// </summary>
/// <param name="estimatedData"></param>
/// <returns></returns>
AVIMNotice Restore(IDictionary<string, object> estimatedData);
}
/// <summary>
/// 从服务端接受到的通知
/// <para>通知泛指消息,对话信息变更(例如加人和被踢等),服务器的 ACK消息回执等</para>
/// </summary>
public class AVIMNotice : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="T:LeanCloud.Realtime.AVIMNotice"/> class.
/// </summary>
public AVIMNotice()
{
}
/// <summary>
/// The name of the command.
/// </summary>
public readonly string CommandName;
public readonly IDictionary<string, object> RawData;
public AVIMNotice(IDictionary<string, object> estimatedData)
{
this.RawData = estimatedData;
this.CommandName = estimatedData["cmd"].ToString();
}
public static bool IsValidLeanCloudProtocol(IDictionary<string, object> estimatedData)
{
if (estimatedData == null) return false;
if (estimatedData.Count == 0) return false;
return true;
}
}
}

View File

@ -0,0 +1,12 @@
using System;
namespace LeanCloud.Realtime {
/// <summary>
/// 撤回消息
/// </summary>
[AVIMMessageClassName("_AVIMRecalledMessagee")]
[AVIMTypedMessageTypeInt(-127)]
public class AVIMRecalledMessage : AVIMTypedMessage {
}
}

View File

@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LeanCloud.Realtime
{
/// <summary>
/// 签名
/// </summary>
public class AVIMSignature
{
/// <summary>
/// 经过 SHA1 以及相关操作参数计算出来的加密字符串
/// </summary>
public string SignatureContent { get; set; }
/// <summary>
/// 服务端时间戳
/// </summary>
public long Timestamp { get; set; }
/// <summary>
/// 随机字符串
/// </summary>
public string Nonce { get; set; }
/// <summary>
/// 构造一个签名
/// </summary>
/// <param name="s"></param>
/// <param name="t"></param>
/// <param name="n"></param>
public AVIMSignature(string s,long t,string n)
{
this.Nonce = n;
this.SignatureContent = s;
this.Timestamp = t;
}
}
}

View File

@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LeanCloud.Realtime
{
/// <summary>
/// Temporary conversation.
/// </summary>
public class AVIMTemporaryConversation : AVIMConversation
{
public DateTime ExpiredAt
{
get
{
if (expiredAt == null)
return DateTime.Now.AddDays(1);
return expiredAt.Value;
}
set
{
expiredAt = value;
}
}
internal AVIMTemporaryConversation(long ttl)
: base(isTemporary: true)
{
this.expiredAt = DateTime.Now.AddDays(1);
}
}
}

View File

@ -0,0 +1,47 @@
using LeanCloud.Realtime.Internal;
using LeanCloud.Storage.Internal;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LeanCloud.Realtime
{
/// <summary>
/// 纯文本信息
/// </summary>
[AVIMMessageClassName("_AVIMTextMessage")]
[AVIMTypedMessageTypeInt(-1)]
public class AVIMTextMessage : AVIMTypedMessage
{
/// <summary>
/// 构建一个文本信息 <see cref="AVIMTextMessage"/> class.
/// </summary>
public AVIMTextMessage()
{
}
/// <summary>
/// 文本类型标记
/// </summary>
[Obsolete("LCType is deprecated, please use AVIMTypedMessageTypeInt instead.")]
[AVIMMessageFieldName("_lctype")]
public int LCType
{
get; set;
}
/// <summary>
/// 构造一个纯文本信息
/// </summary>
/// <param name="textContent"></param>
public AVIMTextMessage(string textContent)
: this()
{
TextContent = textContent;
}
}
}

View File

@ -0,0 +1,205 @@
using LeanCloud.Storage.Internal;
using LeanCloud.Realtime.Internal;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace LeanCloud.Realtime
{
/// <summary>
///
/// </summary>
[AVIMMessageClassName("_AVIMTypedMessage")]
[AVIMTypedMessageTypeInt(0)]
public class AVIMTypedMessage : AVIMMessage, IEnumerable<KeyValuePair<string, object>>
{
/// <summary>
/// Initializes a new instance of the <see cref="T:LeanCloud.Realtime.AVIMTypedMessage"/> class.
/// </summary>
public AVIMTypedMessage()
{
}
/// <summary>
/// 文本内容
/// </summary>
[AVIMMessageFieldName("_lctext")]
public string TextContent
{
get; set;
}
private IDictionary<string, object> estimatedData = new Dictionary<string, object>();
/// <summary>
/// Serialize this instance.
/// </summary>
/// <returns>The serialize.</returns>
public override string Serialize()
{
var result = Encode();
var resultStr = Json.Encode(result);
this.Content = resultStr;
return resultStr;
}
/// <summary>
/// Encode this instance.
/// </summary>
/// <returns>The encode.</returns>
public virtual IDictionary<string, object> Encode()
{
var result = AVRealtime.FreeStyleMessageClassingController.EncodeProperties(this);
var encodedAttrs = PointerOrLocalIdEncoder.Instance.Encode(estimatedData);
result[AVIMProtocol.LCATTRS] = estimatedData;
return result;
}
/// <summary>
/// Validate the specified msgStr.
/// </summary>
/// <returns>The validate.</returns>
/// <param name="msgStr">Message string.</param>
public override bool Validate(string msgStr)
{
try
{
var msg = Json.Parse(msgStr) as IDictionary<string, object>;
return msg.ContainsKey(AVIMProtocol.LCTYPE);
}
catch
{
}
return false;
}
/// <summary>
/// Deserialize the specified msgStr.
/// </summary>
/// <returns>The deserialize.</returns>
/// <param name="msgStr">Message string.</param>
public override IAVIMMessage Deserialize(string msgStr)
{
var msg = Json.Parse(msgStr) as IDictionary<string, object>;
var className = AVRealtime.FreeStyleMessageClassingController.GetClassName(this.GetType());
var PropertyMappings = AVRealtime.FreeStyleMessageClassingController.GetPropertyMappings(className);
var messageFieldProperties = PropertyMappings.Where(prop => msg.ContainsKey(prop.Value))
.Select(prop => Tuple.Create(ReflectionHelpers.GetProperty(this.GetType(), prop.Key), msg[prop.Value]));
foreach (var property in messageFieldProperties)
{
property.Item1.SetValue(this, property.Item2, null);
}
if (msg.ContainsKey(AVIMProtocol.LCATTRS))
{
object attrs = msg[AVIMProtocol.LCATTRS];
this.estimatedData = AVDecoder.Instance.Decode(attrs) as Dictionary<string, object>;
}
return base.Deserialize(msgStr);
}
/// <summary>
/// Gets or sets the <see cref="T:LeanCloud.Realtime.AVIMTypedMessage"/> with the specified key.
/// </summary>
/// <param name="key">Key.</param>
public virtual object this[string key]
{
get
{
if (estimatedData.TryGetValue(key, out object value)) {
return value;
}
return null;
}
set
{
estimatedData[key] = value;
}
}
/// <summary>
/// Merges the custom attributes.
/// </summary>
/// <param name="customAttributes">Custom attributes.</param>
public void MergeCustomAttributes(IDictionary<string, object> customAttributes)
{
this.estimatedData = this.estimatedData.Merge(customAttributes);
}
/// <summary>
/// Gets the enumerator.
/// </summary>
/// <returns>The enumerator.</returns>
public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
{
return estimatedData.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable<KeyValuePair<string, object>>)this).GetEnumerator();
}
}
/// <summary>
/// AVIMMessage decorator.
/// </summary>
public abstract class AVIMMessageDecorator : AVIMTypedMessage
{
/// <summary>
/// Gets or sets the message.
/// </summary>
/// <value>The message.</value>
public AVIMTypedMessage Message { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="T:LeanCloud.Realtime.AVIMMessageDecorator"/> class.
/// </summary>
/// <param name="message">Message.</param>
protected AVIMMessageDecorator(AVIMTypedMessage message)
{
this.Message = message;
}
/// <summary>
/// Gets or sets the content of the message.
/// </summary>
/// <value>The content of the message.</value>
public virtual IDictionary<string, object> MessageContent { get; set; }
/// <summary>
/// Encodes the decorated.
/// </summary>
/// <returns>The decorated.</returns>
public virtual IDictionary<string, object> EncodeDecorated()
{
return Message.Encode();
}
/// <summary>
/// Encode this instance.
/// </summary>
/// <returns>The encode.</returns>
public override IDictionary<string, object> Encode()
{
var decoratedMessageEncoded = EncodeDecorated();
var selfEncoded = base.Encode();
var decoratoEncoded = this.EncodeDecorator();
var resultEncoed = decoratedMessageEncoded.Merge(selfEncoded).Merge(decoratoEncoded);
return resultEncoed;
}
/// <summary>
/// Encodes the decorator.
/// </summary>
/// <returns>The decorator.</returns>
public abstract IDictionary<string, object> EncodeDecorator();
}
}

View File

@ -0,0 +1,14 @@
using System;
namespace LeanCloud.Realtime
{
[AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)]
public sealed class AVIMTypedMessageTypeIntAttribute : Attribute
{
public AVIMTypedMessageTypeIntAttribute(int typeInt)
{
this.TypeInteger = typeInt;
}
public int TypeInteger { get; private set; }
}
}

1282
RTM/RTM/Public/AVRealtime.cs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LeanCloud.Realtime
{
/// <summary>
/// WebSocket 监听服务端事件通知的接口
/// 所有基于协议层的事件监听都需要实现这个接口,然后自定义监听协议。
/// </summary>
public interface IAVIMListener
{
/// <summary>
/// 监听的协议 Hook
/// 例如,消息的协议是 direct 命令,因此消息监听需要判断 <see cref="AVIMNotice.CommandName"/> == "direct" 才可以调用
/// </summary>
/// <param name="notice"></param>
/// <returns></returns>
bool ProtocolHook(AVIMNotice notice);
///// <summary>
///// 如果 <see cref="IAVIMListener.HookFilter"/> 返回 true则会启动 NoticeAction 里面的回调逻辑
///// </summary>
//Action<AVIMNotice> NoticeAction { get; set; }
/// <summary>
/// 如果 <see cref="IAVIMListener.OnNoticeReceived(AVIMNotice)"/> 返回 true则会启动 NoticeAction 里面的回调逻辑
/// </summary>
void OnNoticeReceived(AVIMNotice notice);
}
}

View File

@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LeanCloud.Realtime
{
/// <summary>
/// 消息接口
/// <para>所有消息必须实现这个接口</para>
/// </summary>
public interface IAVIMMessage
{
/// <summary>
/// Serialize this instance.
/// </summary>
/// <returns>The serialize.</returns>
string Serialize();
/// <summary>
/// Validate the specified msgStr.
/// </summary>
/// <returns>The validate.</returns>
/// <param name="msgStr">Message string.</param>
bool Validate(string msgStr);
/// <summary>
/// Deserialize the specified msgStr.
/// </summary>
/// <returns>The deserialize.</returns>
/// <param name="msgStr">Message string.</param>
IAVIMMessage Deserialize(string msgStr);
/// <summary>
/// Gets or sets the conversation identifier.
/// </summary>
/// <value>The conversation identifier.</value>
string ConversationId { get; set; }
/// <summary>
/// Gets or sets from client identifier.
/// </summary>
/// <value>From client identifier.</value>
string FromClientId { get; set; }
/// <summary>
/// Gets or sets the identifier.
/// </summary>
/// <value>The identifier.</value>
string Id { get; set; }
/// <summary>
/// Gets or sets the server timestamp.
/// </summary>
/// <value>The server timestamp.</value>
long ServerTimestamp { get; set; }
/// <summary>
/// Gets or sets the rcp timestamp.
/// </summary>
/// <value>The rcp timestamp.</value>
long RcpTimestamp { get; set; }
long UpdatedAt { get; set; }
#region mention features.
/// <summary>
/// Gets or sets a value indicating whether this <see cref="T:LeanCloud.Realtime.IAVIMMessage"/> mention all.
/// </summary>
/// <value><c>true</c> if mention all; otherwise, <c>false</c>.</value>
bool MentionAll { get; set; }
/// <summary>
/// Gets or sets the mention list.
/// </summary>
/// <value>The mention list.</value>
IEnumerable<string> MentionList { get; set; }
#endregion
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LeanCloud.Realtime
{
public interface ISQLStorage
{
}
}

View File

@ -0,0 +1,131 @@
using LeanCloud;
using LeanCloud.Storage.Internal;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LeanCloud.Realtime
{
/// <summary>
/// 对话操作的签名类型,比如讲一个 client id 加入到对话中
/// <see cref="https://leancloud.cn/docs/realtime_v2.html#群组功能的签名"/>
/// </summary>
public enum ConversationSignatureAction
{
/// <summary>
/// add 加入对话和邀请对方加入对话
/// </summary>
Add,
/// <summary>
/// remove 当前 client Id 离开对话和将其他人踢出对话
/// </summary>
Remove
}
/// <summary>
/// <see cref="https://leancloud.cn/docs/realtime_v2.html#群组功能的签名"/>
/// </summary>
public interface ISignatureFactory
{
/// <summary>
/// 构建登录签名
/// </summary>
/// <param name="clientId">需要登录到云端服务器的 client Id</param>
/// <returns></returns>
Task<AVIMSignature> CreateConnectSignature(string clientId);
/// <summary>
///
/// </summary>
/// <param name="clientId"></param>
/// <param name="targetIds"></param>
/// <returns></returns>
Task<AVIMSignature> CreateStartConversationSignature(string clientId, IEnumerable<string> targetIds);
/// <summary>
///
/// </summary>
/// <param name="conversationId"></param>
/// <param name="clientId"></param>
/// <param name="targetIds"></param>
/// <param name="action">需要签名的操作</param>
/// <returns></returns>
Task<AVIMSignature> CreateConversationSignature(string conversationId, string clientId, IEnumerable<string> targetIds, ConversationSignatureAction action);
}
internal class DefaulSiganatureFactory : ISignatureFactory
{
Task<AVIMSignature> ISignatureFactory.CreateConnectSignature(string clientId)
{
return Task.FromResult<AVIMSignature>(null);
}
Task<AVIMSignature> ISignatureFactory.CreateConversationSignature(string conversationId, string clientId, IEnumerable<string> targetIds, ConversationSignatureAction action)
{
return Task.FromResult<AVIMSignature>(null);
}
Task<AVIMSignature> ISignatureFactory.CreateStartConversationSignature(string clientId, IEnumerable<string> targetIds)
{
return Task.FromResult<AVIMSignature>(null);
}
}
public class LeanEngineSignatureFactory : ISignatureFactory
{
public Task<AVIMSignature> CreateConnectSignature(string clientId)
{
var data = new Dictionary<string, object>();
data.Add("client_id", clientId);
return AVCloud.CallFunctionAsync<IDictionary<string, object>>("connect", data).OnSuccess(_ =>
{
var jsonData = _.Result;
var s = jsonData["signature"].ToString();
var n = jsonData["nonce"].ToString();
var t = long.Parse(jsonData["timestamp"].ToString());
var signature = new AVIMSignature(s, t, n);
return signature;
});
}
public Task<AVIMSignature> CreateStartConversationSignature(string clientId, IEnumerable<string> targetIds)
{
var data = new Dictionary<string, object>();
data.Add("client_id", clientId);
data.Add("members", targetIds.ToList());
return AVCloud.CallFunctionAsync<IDictionary<string, object>>("startConversation", data).OnSuccess(_ =>
{
var jsonData = _.Result;
var s = jsonData["signature"].ToString();
var n = jsonData["nonce"].ToString();
var t = long.Parse(jsonData["timestamp"].ToString());
var signature = new AVIMSignature(s, t, n);
return signature;
});
}
public Task<AVIMSignature> CreateConversationSignature(string conversationId, string clientId, IEnumerable<string> targetIds, ConversationSignatureAction action)
{
var actionList = new string[] { "invite", "kick" };
var data = new Dictionary<string, object>();
data.Add("client_id", clientId);
data.Add("conv_id", conversationId);
data.Add("members", targetIds.ToList());
data.Add("action", actionList[(int)action]);
return AVCloud.CallFunctionAsync<IDictionary<string, object>>("oprateConversation", data).OnSuccess(_ =>
{
var jsonData = _.Result;
var s = jsonData["signature"].ToString();
var n = jsonData["nonce"].ToString();
var t = long.Parse(jsonData["timestamp"].ToString());
var signature = new AVIMSignature(s, t, n);
return signature;
});
}
}
}

View File

@ -0,0 +1,256 @@
using LeanCloud.Storage.Internal;
using LeanCloud.Realtime.Internal;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LeanCloud.Realtime
{
/// <summary>
/// 对话中成员变动的事件参数它提供被操作的对话Conversation操作类型AffectedType
/// 受影响的成员列表AffectedMembers
/// </summary>
public class AVIMOnMembersChangedEventArgs : EventArgs
{
/// <summary>
/// 本次成员变动中被操作的具体对话AVIMConversation的对象
/// </summary>
public AVIMConversation Conversation { get; set; }
/// <summary>
/// 变动的类型
/// </summary>
public AVIMConversationEventType AffectedType { get; internal set; }
/// <summary>
/// 受影响的成员的 Client Ids
/// </summary>
public IList<string> AffectedMembers { get; set; }
/// <summary>
/// 操作人的 Client ClientId
/// </summary>
public string Oprator { get; set; }
/// <summary>
/// 操作的时间,已转化为本地时间
/// </summary>
public DateTime OpratedTime { get; set; }
}
/// <summary>
/// 变动的类型,目前支持如下:
/// 1、Joined当前 Client 主动加入,案例:当 A 主动加入到对话A 将收到 Joined 事件响应,其余的成员收到 MembersJoined 事件响应
/// 2、Left当前 Client 主动退出,案例:当 A 从对话中退出A 将收到 Left 事件响应,其余的成员收到 MembersLeft 事件响应
/// 3、MembersJoined某个成员加入区别于Joined和Kicked案例当 A 把 B 加入到对话中C 将收到 MembersJoined 事件响应
/// 4、MembersLeft某个成员加入区别于Joined和Kicked案例当 A 把 B 从对话中剔除C 将收到 MembersLeft 事件响应
/// 5、Invited当前 Client 被邀请加入,案例:当 A 被 B 邀请加入到对话中A 将收到 Invited 事件响应B 将收到 Joined ,其余的成员收到 MembersJoined 事件响应
/// 6、Kicked当前 Client 被剔除,案例:当 A 被 B 从对话中剔除A 将收到 Kicked 事件响应B 将收到 Left其余的成员收到 MembersLeft 事件响应
/// </summary>
public enum AVIMConversationEventType
{
/// <summary>
/// 自身主动加入
/// </summary>
Joined = 1,
/// <summary>
/// 自身主动离开
/// </summary>
Left,
/// <summary>
/// 他人加入
/// </summary>
MembersJoined,
/// <summary>
/// 他人离开
/// </summary>
MembersLeft,
/// <summary>
/// 自身被邀请加入
/// </summary>
Invited,
/// <summary>
/// 自身被他人剔除
/// </summary>
Kicked
}
#region AVIMMembersJoinListener
//when Members joined or invited by member,this listener will invoke AVIMOnMembersJoinedEventArgs event.
/// <summary>
/// 对话中有成员加入的时候,在改对话中的其他成员都会触发 <see cref="AVIMMembersJoinListener.OnMembersJoined"/> 事件
/// </summary>
public class AVIMMembersJoinListener : IAVIMListener
{
private EventHandler<AVIMOnMembersJoinedEventArgs> m_OnMembersJoined;
/// <summary>
/// 有成员加入到对话时,触发的事件
/// </summary>
public event EventHandler<AVIMOnMembersJoinedEventArgs> OnMembersJoined
{
add
{
m_OnMembersJoined += value;
}
remove
{
m_OnMembersJoined -= value;
}
}
public virtual void OnNoticeReceived(AVIMNotice notice)
{
if (m_OnMembersJoined != null)
{
var joinedMembers = AVDecoder.Instance.DecodeList<string>(notice.RawData["m"]);
var ivitedBy = notice.RawData["initBy"].ToString();
var conersationId = notice.RawData["cid"].ToString();
var args = new AVIMOnMembersJoinedEventArgs()
{
ConversationId = conersationId,
InvitedBy = ivitedBy,
JoinedMembers = joinedMembers
};
m_OnMembersJoined.Invoke(this, args);
}
}
public virtual bool ProtocolHook(AVIMNotice notice)
{
if (notice.CommandName != "conv") return false;
if (!notice.RawData.ContainsKey("op")) return false;
var op = notice.RawData["op"].ToString();
if (!op.Equals("members-joined")) return false;
return true;
}
}
#endregion
#region AVIMMembersLeftListener
// when Members left or kicked by member,this listener will invoke AVIMOnMembersJoinedEventArgs event.
/// <summary>
/// 对话中有成员加入的时候,在改对话中的其他成员都会触发 <seealso cref="AVIMMembersLeftListener.OnMembersLeft"/>OnMembersJoined 事件
/// </summary>
public class AVIMMembersLeftListener : IAVIMListener
{
private EventHandler<AVIMOnMembersLeftEventArgs> m_OnMembersLeft;
/// <summary>
/// 有成员加入到对话时,触发的事件
/// </summary>
public event EventHandler<AVIMOnMembersLeftEventArgs> OnMembersLeft
{
add
{
m_OnMembersLeft += value;
}
remove
{
m_OnMembersLeft -= value;
}
}
public virtual void OnNoticeReceived(AVIMNotice notice)
{
if (m_OnMembersLeft != null)
{
var leftMembers = AVDecoder.Instance.DecodeList<string>(notice.RawData["m"]);
var kickedBy = notice.RawData["initBy"].ToString();
var conersationId = notice.RawData["cid"].ToString();
var args = new AVIMOnMembersLeftEventArgs()
{
ConversationId = conersationId,
KickedBy = kickedBy,
LeftMembers = leftMembers
};
m_OnMembersLeft.Invoke(this, args);
}
}
public virtual bool ProtocolHook(AVIMNotice notice)
{
if (notice.CommandName != "conv") return false;
if (!notice.RawData.ContainsKey("op")) return false;
var op = notice.RawData["op"].ToString();
if (!op.Equals("members-left")) return false;
return true;
}
}
#endregion
#region AVIMInvitedListener
public class AVIMInvitedListener : IAVIMListener
{
private EventHandler<AVIMOnInvitedEventArgs> m_OnInvited;
public event EventHandler<AVIMOnInvitedEventArgs> OnInvited {
add {
m_OnInvited += value;
} remove {
m_OnInvited -= value;
}
}
public void OnNoticeReceived(AVIMNotice notice)
{
if (m_OnInvited != null)
{
var ivitedBy = notice.RawData["initBy"].ToString();
var conersationId = notice.RawData["cid"].ToString();
var args = new AVIMOnInvitedEventArgs()
{
ConversationId = conersationId,
InvitedBy = ivitedBy,
};
m_OnInvited.Invoke(this, args);
}
}
public bool ProtocolHook(AVIMNotice notice)
{
if (notice.CommandName != "conv") return false;
if (!notice.RawData.ContainsKey("op")) return false;
var op = notice.RawData["op"].ToString();
if (!op.Equals("joined")) return false;
return true;
}
}
#endregion
#region AVIMKickedListener
public class AVIMKickedListener : IAVIMListener
{
private EventHandler<AVIMOnKickedEventArgs> m_OnKicked;
public event EventHandler<AVIMOnKickedEventArgs> OnKicked {
add {
m_OnKicked += value;
} remove {
m_OnKicked -= value;
}
}
public void OnNoticeReceived(AVIMNotice notice)
{
if (m_OnKicked != null)
{
var kickcdBy = notice.RawData["initBy"].ToString();
var conersationId = notice.RawData["cid"].ToString();
var args = new AVIMOnKickedEventArgs()
{
ConversationId = conersationId,
KickedBy = kickcdBy,
};
m_OnKicked.Invoke(this, args);
}
}
public bool ProtocolHook(AVIMNotice notice)
{
if (notice.CommandName != "conv") return false;
if (!notice.RawData.ContainsKey("op")) return false;
var op = notice.RawData["op"].ToString();
if (!op.Equals("left")) return false;
return true;
}
}
#endregion
}

View File

@ -0,0 +1,145 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using LeanCloud.Realtime.Internal;
namespace LeanCloud.Realtime
{
internal class ConversationUnreadListener : IAVIMListener
{
internal class UnreadConversationNotice : IEqualityComparer<UnreadConversationNotice>
{
internal readonly object mutex = new object();
internal IAVIMMessage LastUnreadMessage { get; set; }
internal string ConvId { get; set; }
internal int UnreadCount { get; set; }
public bool Equals(UnreadConversationNotice x, UnreadConversationNotice y)
{
return x.ConvId == y.ConvId;
}
public int GetHashCode(UnreadConversationNotice obj)
{
return obj.ConvId.GetHashCode();
}
internal void AutomicIncrement()
{
lock (mutex)
{
UnreadCount++;
}
}
}
internal static readonly object sMutex = new object();
internal static long NotifTime;
internal static HashSet<UnreadConversationNotice> UnreadConversations;
static ConversationUnreadListener()
{
UnreadConversations = new HashSet<UnreadConversationNotice>(new UnreadConversationNotice());
NotifTime = DateTime.Now.ToUnixTimeStamp();
}
internal static void UpdateNotice(IAVIMMessage message)
{
lock (sMutex)
{
var convValidators = UnreadConversations.Where(c => c.ConvId == message.ConversationId);
if (convValidators != null)
{
if (convValidators.Count() > 0)
{
var currentNotice = convValidators.FirstOrDefault();
currentNotice.AutomicIncrement();
currentNotice.LastUnreadMessage = message;
}
else
{
var currentThread = new UnreadConversationNotice();
currentThread.ConvId = message.ConversationId;
currentThread.LastUnreadMessage = message;
currentThread.AutomicIncrement();
UnreadConversations.Add(currentThread);
}
}
}
}
internal static void ClearUnread(string convId)
{
UnreadConversations.Remove(Get(convId));
}
internal static IEnumerable<string> FindAllConvIds()
{
lock (sMutex)
{
return ConversationUnreadListener.UnreadConversations.Select(c => c.ConvId);
}
}
internal static UnreadConversationNotice Get(string convId)
{
lock (sMutex)
{
var unreadValidator = ConversationUnreadListener.UnreadConversations.Where(c => c.ConvId == convId);
if (unreadValidator != null)
{
if (unreadValidator.Count() > 0)
{
var notice = unreadValidator.FirstOrDefault();
return notice;
}
}
return null;
}
}
public void OnNoticeReceived(AVIMNotice notice)
{
lock (sMutex)
{
if (notice.RawData.ContainsKey("convs"))
{
var unreadRawData = notice.RawData["convs"] as List<object>;
if (notice.RawData.ContainsKey("notifTime"))
{
long.TryParse(notice.RawData["notifTime"].ToString(), out NotifTime);
}
foreach (var data in unreadRawData)
{
var dataMap = data as IDictionary<string, object>;
if (dataMap != null)
{
var convId = dataMap["cid"].ToString();
var ucn = Get(convId);
if (ucn == null) ucn = new UnreadConversationNotice();
ucn.ConvId = convId;
var unreadCount = 0;
Int32.TryParse(dataMap["unread"].ToString(), out unreadCount);
ucn.UnreadCount = unreadCount;
#region restore last message for the conversation
if (dataMap.ContainsKey("data"))
{
var msgStr = dataMap["data"].ToString();
var messageObj = AVRealtime.FreeStyleMessageClassingController.Instantiate(msgStr, dataMap);
ucn.LastUnreadMessage = messageObj;
}
UnreadConversations.Add(ucn);
#endregion
}
}
}
}
}
public bool ProtocolHook(AVIMNotice notice)
{
return notice.CommandName == "unread";
}
}
}

View File

@ -0,0 +1,28 @@
using System;
namespace LeanCloud.Realtime {
/// <summary>
/// 强制被踢下线处理
/// </summary>
internal class GoAwayListener : IAVIMListener {
Action onGoAway;
public event Action OnGoAway {
add {
onGoAway += value;
}
remove {
onGoAway -= value;
}
}
public void OnNoticeReceived(AVIMNotice notice) {
// TODO 退出并清理路由缓存
onGoAway?.Invoke();
}
public bool ProtocolHook(AVIMNotice notice) {
return notice.CommandName == "goaway";
}
}
}

View File

@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LeanCloud.Realtime
{
internal delegate void OnMessagePatch(IEnumerable<IAVIMMessage> messages);
internal class MessagePatchListener : IAVIMListener
{
public OnMessagePatch OnReceived { get; set; }
public void OnNoticeReceived(AVIMNotice notice)
{
ICollection<IAVIMMessage> patchedMessages = new List<IAVIMMessage>();
var msgObjs = notice.RawData["patches"] as IList<object>;
if (msgObjs != null)
{
foreach (var msgObj in msgObjs)
{
var msgData = msgObj as IDictionary<string, object>;
if (msgData != null)
{
var msgStr = msgData["data"] as string;
var message = AVRealtime.FreeStyleMessageClassingController.Instantiate(msgStr, msgData);
patchedMessages.Add(message);
}
}
}
if (OnReceived != null)
{
if (patchedMessages.Count > 0)
{
this.OnReceived(patchedMessages);
}
}
}
public bool ProtocolHook(AVIMNotice notice)
{
if (notice.CommandName != "patch") return false;
if (!notice.RawData.ContainsKey("op")) return false;
if (notice.RawData["op"].ToString() != "modify") return false;
return true;
}
}
}

View File

@ -0,0 +1,42 @@
using LeanCloud.Storage.Internal;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace LeanCloud.Realtime
{
internal class OfflineMessageListener : IAVIMListener
{
private EventHandler<AVIMMessageEventArgs> m_OnOfflineMessageReceived;
public event EventHandler<AVIMMessageEventArgs> OnOfflineMessageReceived
{
add
{
m_OnOfflineMessageReceived += value;
}
remove
{
m_OnOfflineMessageReceived -= value;
}
}
public void OnNoticeReceived(AVIMNotice notice)
{
if (m_OnOfflineMessageReceived != null)
{
var msgStr = notice.RawData["msg"].ToString();
var iMessage = AVRealtime.FreeStyleMessageClassingController.Instantiate(msgStr, notice.RawData);
var args = new AVIMMessageEventArgs(iMessage);
m_OnOfflineMessageReceived.Invoke(this, args);
}
}
public bool ProtocolHook(AVIMNotice notice)
{
if (notice.CommandName != "direct") return false;
if (!notice.RawData.ContainsKey("offline")) return false;
return true;
}
}
}

View File

@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace LeanCloud.Realtime
{
internal class SessionListener : IAVIMListener
{
private Action<int, string, string> _onSessionClosed;
public event Action<int, string, string> OnSessionClosed
{
add
{
_onSessionClosed += value;
}
remove
{
_onSessionClosed -= value;
}
}
public void OnNoticeReceived(AVIMNotice notice)
{
var code = 0;
if (notice.RawData.ContainsKey("code"))
{
int.TryParse(notice.RawData["code"].ToString(), out code);
}
var reason = "";
if (notice.RawData.ContainsKey("reason"))
{
reason = notice.RawData["reason"].ToString();
}
var detail = "";
if (notice.RawData.ContainsKey("detail"))
{
detail = notice.RawData["detail"].ToString();
}
if (_onSessionClosed != null)
{
_onSessionClosed(code, reason, detail);
}
}
public bool ProtocolHook(AVIMNotice notice)
{
if (notice.CommandName != "session") return false;
if (!notice.RawData.ContainsKey("op")) return false;
if (notice.RawData.ContainsKey("i")) return false;
if (notice.RawData["op"].ToString() != "closed") return false;
return true;
}
}
}

17
RTM/RTM/RTM.csproj Normal file
View File

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Class1.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Storage\Storage\Storage.csproj" />
<ProjectReference Include="..\..\AppRouter\AppRouter\AppRouter.csproj" />
</ItemGroup>
<ItemGroup>
<None Remove="Internal\WebSocket\Unity\websocket-sharp.dll" />
</ItemGroup>
</Project>

View File

@ -1,26 +1,31 @@
using NUnit.Framework;
using LeanCloud;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace LeanCloudTests {
public class FileTest {
string saveFileId;
[SetUp]
public void SetUp() {
Utils.InitNorthChina();
//Utils.InitEastChina();
//Utils.InitOldEastChina();
//Utils.InitUS();
}
[Test]
[Test, Order(0)]
public async Task SaveImage() {
AVFile file = new AVFile("hello.png", File.ReadAllBytes("../../../assets/hello.png"));
await file.SaveAsync();
Assert.NotNull(file.ObjectId);
saveFileId = file.ObjectId;
TestContext.Out.WriteLine($"file: {file.ObjectId}, {file.Url}");
}
[Test]
[Test, Order(1)]
public async Task SaveBigFile() {
AVFile file = new AVFile("test.apk", File.ReadAllBytes("../../../assets/test.apk"));
await file.SaveAsync();
@ -28,7 +33,7 @@ namespace LeanCloudTests {
TestContext.Out.WriteLine($"file: {file.ObjectId}, {file.Url}");
}
[Test]
[Test, Order(2)]
public async Task SaveUrl() {
AVFile file = new AVFile("test.jpg", "http://pic33.nipic.com/20131007/13639685_123501617185_2.jpg");
await file.SaveAsync();
@ -36,17 +41,17 @@ namespace LeanCloudTests {
TestContext.Out.WriteLine($"file: {file.ObjectId}, {file.Url}");
}
[Test]
[Test, Order(3)]
public async Task Thumbnail() {
AVFile file = await AVFile.GetFileWithObjectIdAsync("5d64ac55d5de2b006c1fe3d8");
AVQuery<AVFile> query = new AVQuery<AVFile>();
AVFile file = await query.GetAsync(saveFileId, CancellationToken.None);
Assert.NotNull(file);
TestContext.Out.WriteLine($"url: {file.Url}");
TestContext.Out.WriteLine($"thumbnail url: {file.GetThumbnailUrl(28, 28)}");
}
[Test]
[Test, Order(4)]
public async Task DeleteFile() {
await AVUser.LogInAsync("111111", "111111");
AVFile file = new AVFile("hello.png", File.ReadAllBytes("../../../assets/hello.png"));
await file.SaveAsync();
Assert.NotNull(file.ObjectId);

View File

@ -0,0 +1,27 @@
using System;
namespace Storage.Test {
public class JustTest {
public class Animal {
}
public class Dog : Animal {
}
public class Walk<T> where T : Animal {
public virtual T Do() {
return default;
}
}
public class Run : Walk<Dog> {
public override Dog Do() {
return base.Do();
}
}
public JustTest() {
}
}
}

View File

@ -14,7 +14,9 @@ namespace LeanCloudTests {
[Test]
public async Task BasicQuery() {
var query = new AVQuery<AVObject>("Foo");
query.WhereEqualTo("content", "hello");
query.WhereGreaterThanOrEqualTo("a", 100);
query.WhereLessThanOrEqualTo("a", 100);
//query.WhereEqualTo("content", "hello");
var results = await query.FindAsync();
foreach (var result in results) {
TestContext.Out.WriteLine(result.ObjectId);
@ -55,5 +57,23 @@ namespace LeanCloudTests {
TestContext.Out.WriteLine(result.ObjectId);
}
}
[Test]
public async Task OrPro() {
AVQuery<AVObject> q1 = AVQuery<AVObject>.Or(new List<AVQuery<AVObject>> {
new AVQuery<AVObject>("Account").WhereEqualTo("balance", 100)
});
AVQuery<AVObject> q2 = AVQuery<AVObject>.Or(new List<AVQuery<AVObject>> {
new AVQuery<AVObject>("Account").WhereEqualTo("balance", 200)
});
AVQuery<AVObject> query = AVQuery<AVObject>.Or(new List<AVQuery<AVObject>> {
q1, q2
});
query.WhereEqualTo("balance", 100);
IEnumerable<AVObject> results = await query.FindAsync();
foreach (AVObject result in results) {
TestContext.Out.WriteLine(result.ObjectId);
}
}
}
}

View File

@ -20,6 +20,14 @@ namespace LeanCloudTests {
}
}
public static void InitOldEastChina(bool master = false) {
if (master) {
Init("SpT4SjWdvM9TSvCTKk6rqYQ9-9Nh9j0Va", "4NvN2OfdsWFC7qzzNcNS6paS", "https://4eTwHdYh.api.lncldapi.com", "https://4eTwHdYh.engine.lncldapi.com", "eqEp4n89h4zanWFskDDpIwL4");
} else {
Init("SpT4SjWdvM9TSvCTKk6rqYQ9-9Nh9j0Va", "4NvN2OfdsWFC7qzzNcNS6paS", "https://4eTwHdYh.api.lncldapi.com", "https://4eTwHdYh.engine.lncldapi.com");
}
}
public static void InitUS(bool master = false) {
if (master) {
Init("MFAS1GnOyomRLSQYRaxdgdPz-MdYXbMMI", "p42JUxdxb95K5G8187t5ba3l", "https://MFAS1GnO.api.lncldglobal.com", "https://MFAS1GnO.engine.lncldglobal.com", "Ahb1wdFLwMgKwEaEicHRXbCY");

View File

@ -0,0 +1,97 @@
using System;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Linq;
using LeanCloud.Storage.Internal;
namespace LeanCloud {
public class AVQuery2<T> where T : AVObject {
public string ClassName {
get; set;
}
internal string Op {
get; set;
}
/// <summary>
/// { key: { op: value } }
/// </summary>
public Dictionary<int, QueryCondition> Where {
get; set;
}
internal ReadOnlyCollection<string> OrderBy {
get; set;
}
internal ReadOnlyCollection<string> Includes {
get; set;
}
internal ReadOnlyCollection<string> SelectedKeys {
get; set;
}
internal string RedirectClassNameForKey {
get; set;
}
internal int? Skip {
get; set;
}
internal int? Limit {
get; set;
}
public AVQuery2() {
Op = "$and";
Where = new Dictionary<int, QueryCondition>();
}
public static AVQuery2<T> And(IEnumerable<AVQuery2<T>> queries) {
AVQuery2<T> combination = new AVQuery2<T>();
if (queries != null) {
foreach (AVQuery2<T> query in queries) {
query.BuildWhere();
}
}
return combination;
}
#region where
public AVQuery2<T> WhereEqualTo(string key, object value) {
AddCondition(key, "$eq", value);
return this;
}
#endregion
IDictionary<string, object> BuildWhere() {
if (Where.Count == 0) {
return new Dictionary<string, object>();
}
if (Where.Count == 1) {
return Where.Values.First().ToDictionary();
}
List<IDictionary<string, object>> conditions = new List<IDictionary<string, object>>();
foreach (QueryCondition condition in Where.Values) {
conditions.Add(condition.ToDictionary());
}
return new Dictionary<string, object> {
{ Op, conditions }
};
}
void AddCondition(string key, string op, object value) {
QueryCondition cond = new QueryCondition {
Key = key,
Op = op,
Value = value
};
Where[cond.GetHashCode()] = cond;
}
}
}

View File

@ -114,7 +114,7 @@ namespace LeanCloud.Storage.Internal
{
if (className == "_File")
{
return AVFile.CreateWithoutData(objectId);
return AVFile.CreateWithoutData("_File", objectId);
}
return AVObject.CreateWithoutData(className, objectId);
}
@ -131,8 +131,8 @@ namespace LeanCloud.Storage.Internal
protected virtual object DecodeAVFile(IDictionary<string, object> dict)
{
var objectId = dict["objectId"] as string;
var file = AVFile.CreateWithoutData(objectId);
file.MergeFromJSON(dict);
var file = AVFile.CreateWithoutData("_File", objectId);
//file.MergeFromJSON(dict);
return file;
}

View File

@ -17,26 +17,26 @@ namespace LeanCloud.Storage.Internal {
const string QCloud = "qcloud";
const string AWS = "s3";
public async Task<FileState> SaveAsync(FileState state,
Stream dataStream,
IProgress<AVUploadProgressEventArgs> progress,
CancellationToken cancellationToken = default) {
if (state.Url != null) {
return await SaveWithUrl(state);
}
//public async Task<FileState> SaveAsync(FileState state,
// Stream dataStream,
// IProgress<AVUploadProgressEventArgs> progress,
// CancellationToken cancellationToken = default) {
// if (state.Url != null) {
// return await SaveWithUrl(state);
// }
var data = await GetFileToken(state, cancellationToken);
var fileToken = data.Item2;
var provider = fileToken["provider"] as string;
switch (provider) {
case QCloud:
return await new QCloudUploader().Upload(state, dataStream, fileToken, progress, cancellationToken);
case AWS:
return await new AWSUploader().Upload(state, dataStream, fileToken, progress, cancellationToken);
default:
return await new QiniuUploader().Upload(state, dataStream, fileToken, progress, cancellationToken);
}
}
// var data = await GetFileToken(state, cancellationToken);
// var fileToken = data.Item2;
// var provider = fileToken["provider"] as string;
// switch (provider) {
// case QCloud:
// return await new QCloudUploader().Upload(state, dataStream, fileToken, progress, cancellationToken);
// case AWS:
// return await new AWSUploader().Upload(state, dataStream, fileToken, progress, cancellationToken);
// default:
// return await new QiniuUploader().Upload(state, dataStream, fileToken, progress, cancellationToken);
// }
//}
public async Task DeleteAsync(FileState state, CancellationToken cancellationToken) {
var command = new AVCommand {
@ -75,14 +75,13 @@ namespace LeanCloud.Storage.Internal {
return state;
}
internal async Task<Tuple<HttpStatusCode, IDictionary<string, object>>> GetFileToken(FileState fileState, CancellationToken cancellationToken) {
string str = fileState.Name;
internal async Task<Tuple<HttpStatusCode, IDictionary<string, object>>> GetFileToken(string name, IDictionary<string, object> metaData, CancellationToken cancellationToken = default) {
IDictionary<string, object> parameters = new Dictionary<string, object> {
{ "name", str },
{ "key", GetUniqueName(fileState) },
{ "name", name },
{ "key", GetUniqueName(name) },
{ "__type", "File" },
{ "mime_type", AVFile.GetMIMEType(str) },
{ "metaData", fileState.MetaData }
{ "mime_type", AVFile.GetMIMEType(name) },
{ "metaData", metaData }
};
var command = new AVCommand {
@ -90,7 +89,7 @@ namespace LeanCloud.Storage.Internal {
Method = HttpMethod.Post,
Content = parameters
};
return await AVPlugins.Instance.CommandRunner.RunCommandAsync<IDictionary<string, object>>(command);
return await AVPlugins.Instance.CommandRunner.RunCommandAsync<IDictionary<string, object>>(command, cancellationToken);
}
public async Task<FileState> GetAsync(string objectId, CancellationToken cancellationToken) {
@ -108,9 +107,9 @@ namespace LeanCloud.Storage.Internal {
};
}
internal static string GetUniqueName(FileState state) {
internal static string GetUniqueName(string name) {
string key = Guid.NewGuid().ToString();
string extension = Path.GetExtension(state.Name);
string extension = Path.GetExtension(name);
key += extension;
return key;
}

View File

@ -7,30 +7,33 @@ using System.Net.Http;
using System.Net.Http.Headers;
namespace LeanCloud.Storage.Internal {
internal class AWSUploader : IFileUploader {
public async Task<FileState> Upload(FileState state, Stream dataStream, IDictionary<string, object> fileToken, IProgress<AVUploadProgressEventArgs> progress,
CancellationToken cancellationToken) {
var uploadUrl = fileToken["upload_url"].ToString();
state.ObjectId = fileToken["objectId"].ToString();
string url = fileToken["url"] as string;
state.Url = new Uri(url, UriKind.Absolute);
return await PutFile(state, uploadUrl, dataStream);
internal class AWSUploader {
internal string UploadUrl {
get; set;
}
internal async Task<FileState> PutFile(FileState state, string uploadUrl, Stream dataStream) {
internal string MimeType {
get; set;
}
internal Stream Stream {
get; set;
}
internal async Task Upload(CancellationToken cancellationToken = default) {
HttpClient client = new HttpClient();
HttpRequestMessage request = new HttpRequestMessage {
RequestUri = new Uri(uploadUrl),
RequestUri = new Uri(UploadUrl),
Method = HttpMethod.Put,
Content = new StreamContent(dataStream)
Content = new StreamContent(Stream)
};
request.Headers.Add("Cache-Control", "public, max-age=31536000");
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(state.MimeType);
request.Content.Headers.ContentLength = dataStream.Length;
await client.SendAsync(request);
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(MimeType);
request.Content.Headers.ContentLength = Stream.Length;
HttpResponseMessage response = await client.SendAsync(request, cancellationToken);
response.Dispose();
client.Dispose();
request.Dispose();
return state;
}
}
}

View File

@ -3,44 +3,54 @@ using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace LeanCloud.Storage.Internal {
internal class QCloudUploader : IFileUploader {
internal class QCloudUploader {
private object mutex = new object();
FileState fileState;
Stream data;
string bucket;
string token;
string uploadUrl;
bool done;
private long sliceSize = (long)CommonSize.KB512;
public async Task<FileState> Upload(FileState state, Stream dataStream, IDictionary<string, object> fileToken, IProgress<AVUploadProgressEventArgs> progress, CancellationToken cancellationToken) {
fileState = state;
data = dataStream;
uploadUrl = fileToken["upload_url"].ToString();
token = fileToken["token"].ToString();
fileState.ObjectId = fileToken["objectId"].ToString();
bucket = fileToken["bucket"].ToString();
internal string FileName {
get; set;
}
internal string UploadUrl {
get; set;
}
internal string Token {
get; set;
}
internal string Bucket {
get; set;
}
internal Stream Stream {
get; set;
}
internal async Task Upload(CancellationToken cancellationToken = default) {
var result = await FileSlice(cancellationToken);
if (done) {
return state;
return;
}
var response = result.Item2;
var response = result;
var resumeData = response["data"] as IDictionary<string, object>;
if (resumeData.ContainsKey("access_url")) {
return state;
return;
}
var sliceSession = resumeData["session"].ToString();
var sliceOffset = long.Parse(resumeData["offset"].ToString());
return await UploadSlice(sliceSession, sliceOffset, dataStream, progress, cancellationToken);
await UploadSlice(sliceSession, sliceOffset, Stream, null, cancellationToken);
}
async Task<FileState> UploadSlice(
async Task UploadSlice(
string sessionId,
long offset,
Stream dataStream,
@ -57,7 +67,7 @@ namespace LeanCloud.Storage.Internal {
}
if (offset == dataLength) {
return fileState;
return;
}
var sliceFile = GetNextBinary(offset, dataStream);
@ -65,15 +75,15 @@ namespace LeanCloud.Storage.Internal {
offset += sliceFile.Length;
if (offset == dataLength) {
done = true;
return fileState;
return;
}
var response = result.Item2;
var response = result;
var resumeData = response["data"] as IDictionary<string, object>;
var sliceSession = resumeData["session"].ToString();
return await UploadSlice(sliceSession, offset, dataStream, progress, cancellationToken);
await UploadSlice(sliceSession, offset, dataStream, progress, cancellationToken);
}
Task<Tuple<HttpStatusCode, IDictionary<string, object>>> ExcuteUpload(string sessionId, long offset, byte[] sliceFile, CancellationToken cancellationToken) {
Task<IDictionary<string, object>> ExcuteUpload(string sessionId, long offset, byte[] sliceFile, CancellationToken cancellationToken) {
var body = new Dictionary<string, object>();
body.Add("op", "upload_slice");
body.Add("session", sessionId);
@ -82,22 +92,19 @@ namespace LeanCloud.Storage.Internal {
return PostToQCloud(body, sliceFile, cancellationToken);
}
async Task<Tuple<HttpStatusCode, IDictionary<string, object>>> FileSlice(CancellationToken cancellationToken) {
async Task<IDictionary<string, object>> FileSlice(CancellationToken cancellationToken) {
SHA1CryptoServiceProvider sha1 = new SHA1CryptoServiceProvider();
var body = new Dictionary<string, object>();
if (data.Length <= (long)CommonSize.KB512) {
if (Stream.Length <= (long)CommonSize.KB512) {
body.Add("op", "upload");
body.Add("sha", HexStringFromBytes(sha1.ComputeHash(data)));
var wholeFile = GetNextBinary(0, data);
body.Add("sha", HexStringFromBytes(sha1.ComputeHash(Stream)));
var wholeFile = GetNextBinary(0, Stream);
var result = await PostToQCloud(body, wholeFile, cancellationToken);
if (result.Item1 == HttpStatusCode.OK) {
done = true;
}
return result;
} else {
body.Add("op", "upload_slice");
body.Add("filesize", data.Length);
body.Add("sha", HexStringFromBytes(sha1.ComputeHash(data)));
body.Add("filesize", Stream.Length);
body.Add("sha", HexStringFromBytes(sha1.ComputeHash(Stream)));
body.Add("slice_size", (long)CommonSize.KB512);
}
@ -121,36 +128,32 @@ namespace LeanCloud.Storage.Internal {
return HexStringFromBytes(hashBytes);
}
async Task<Tuple<HttpStatusCode, IDictionary<string, object>>> PostToQCloud(
async Task<IDictionary<string, object>> PostToQCloud(
Dictionary<string, object> body,
byte[] sliceFile,
CancellationToken cancellationToken) {
IList<KeyValuePair<string, string>> sliceHeaders = new List<KeyValuePair<string, string>>();
sliceHeaders.Add(new KeyValuePair<string, string>("Authorization", this.token));
string contentType;
long contentLength;
var tempStream = HttpUploadFile(sliceFile, fileState.CloudName, out contentType, out contentLength, body);
sliceHeaders.Add(new KeyValuePair<string, string>("Content-Type", contentType));
var tempStream = HttpUploadFile(sliceFile, FileName, out contentType, out contentLength, body);
var client = new HttpClient();
var request = new HttpRequestMessage {
RequestUri = new Uri(uploadUrl),
RequestUri = new Uri(UploadUrl),
Method = HttpMethod.Post,
Content = new StreamContent(tempStream)
};
foreach (var header in sliceHeaders) {
request.Headers.Add(header.Key, header.Value);
}
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Authorization", Token);
request.Content.Headers.Add("Content-Type", contentType);
var response = await client.SendAsync(request);
client.Dispose();
request.Dispose();
var content = await response.Content.ReadAsStringAsync();
response.Dispose();
// TODO 修改反序列化返回
return await JsonUtils.DeserializeObjectAsync<Tuple<HttpStatusCode, IDictionary<string, object>>>(content);
// 修改反序列化返回
return await JsonUtils.DeserializeObjectAsync<IDictionary<string, object>>(content, new LeanCloudJsonConverter());
}
public static Stream HttpUploadFile(byte[] file, string fileName, out string contentType, out long contentLength, IDictionary<string, object> nvc) {
string boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x");

View File

@ -7,7 +7,6 @@ using System.Threading;
using System.Threading.Tasks;
using System.Net.Http;
using System.Net.Http.Headers;
using Newtonsoft.Json;
namespace LeanCloud.Storage.Internal {
internal enum CommonSize : long {
@ -17,83 +16,107 @@ namespace LeanCloud.Storage.Internal {
KB256 = 1024 * 1024 / 4
}
internal class QiniuUploader : IFileUploader {
internal class QiniuUploader {
private static readonly int BLOCKSIZE = 1024 * 1024 * 4;
internal static string UP_HOST = "https://up.qbox.me";
private readonly object mutex = new object();
public async Task<FileState> Upload(FileState state, Stream dataStream, IDictionary<string, object> fileToken, IProgress<AVUploadProgressEventArgs> progress, CancellationToken cancellationToken) {
state.frozenData = dataStream;
state.CloudName = fileToken["key"] as string;
MergeFromJSON(state, fileToken);
await UploadNextChunk(state, dataStream, string.Empty, 0, progress);
return state;
public int counter;
public Stream frozenData;
public string bucketId;
public string bucket;
public string token;
public long completed;
public List<string> block_ctxes = new List<string>();
internal string Key {
get; set;
}
async Task UploadNextChunk(FileState state, Stream dataStream, string context, long offset, IProgress<AVUploadProgressEventArgs> progress) {
var totalSize = dataStream.Length;
var remainingSize = totalSize - state.completed;
internal string Token {
get; set;
}
internal string MimeType {
get; set;
}
internal IDictionary<string, object> MetaData {
get; set;
}
internal Stream Stream {
get; set;
}
internal async Task Upload(CancellationToken cancellationToken = default) {
await UploadNextChunk(string.Empty, 0, null);
}
async Task UploadNextChunk(string context, long offset, IProgress<AVUploadProgressEventArgs> progress) {
var totalSize = Stream.Length;
var remainingSize = totalSize - completed;
if (progress != null) {
lock (mutex) {
progress.Report(new AVUploadProgressEventArgs() {
Progress = AVFileController.CalcProgress(state.completed, totalSize)
Progress = AVFileController.CalcProgress(completed, totalSize)
});
}
}
if (state.completed == totalSize) {
await QiniuMakeFile(state, state.frozenData, state.token, state.CloudName, totalSize, state.block_ctxes.ToArray(), CancellationToken.None);
} else if (state.completed % BLOCKSIZE == 0) {
var firstChunkBinary = GetChunkBinary(state.completed, dataStream);
if (completed == totalSize) {
await QiniuMakeFile(totalSize, block_ctxes.ToArray(), CancellationToken.None);
} else if (completed % BLOCKSIZE == 0) {
var firstChunkBinary = GetChunkBinary();
var blockSize = remainingSize > BLOCKSIZE ? BLOCKSIZE : remainingSize;
var result = await MakeBlock(state, firstChunkBinary, blockSize);
var result = await MakeBlock(firstChunkBinary, blockSize);
var dict = result;
var ctx = dict["ctx"].ToString();
offset = long.Parse(dict["offset"].ToString());
var host = dict["host"].ToString();
state.completed += firstChunkBinary.Length;
if (state.completed % BLOCKSIZE == 0 || state.completed == totalSize) {
state.block_ctxes.Add(ctx);
completed += firstChunkBinary.Length;
if (completed % BLOCKSIZE == 0 || completed == totalSize) {
block_ctxes.Add(ctx);
}
await UploadNextChunk(state, dataStream, ctx, offset, progress);
await UploadNextChunk(ctx, offset, progress);
} else {
var chunkBinary = GetChunkBinary(state.completed, dataStream);
var result = await PutChunk(state, chunkBinary, context, offset);
var chunkBinary = GetChunkBinary();
var result = await PutChunk(chunkBinary, context, offset);
var dict = result;
var ctx = dict["ctx"].ToString();
offset = long.Parse(dict["offset"].ToString());
var host = dict["host"].ToString();
state.completed += chunkBinary.Length;
if (state.completed % BLOCKSIZE == 0 || state.completed == totalSize) {
state.block_ctxes.Add(ctx);
completed += chunkBinary.Length;
if (completed % BLOCKSIZE == 0 || completed == totalSize) {
block_ctxes.Add(ctx);
}
await UploadNextChunk(state, dataStream, ctx, offset, progress);
await UploadNextChunk(ctx, offset, progress);
}
}
byte[] GetChunkBinary(long completed, Stream dataStream) {
byte[] GetChunkBinary() {
long chunkSize = (long)CommonSize.MB1;
if (completed + chunkSize > dataStream.Length) {
chunkSize = dataStream.Length - completed;
if (completed + chunkSize > Stream.Length) {
chunkSize = Stream.Length - completed;
}
byte[] chunkBinary = new byte[chunkSize];
dataStream.Seek(completed, SeekOrigin.Begin);
dataStream.Read(chunkBinary, 0, (int)chunkSize);
Stream.Seek(completed, SeekOrigin.Begin);
Stream.Read(chunkBinary, 0, (int)chunkSize);
return chunkBinary;
}
IList<KeyValuePair<string, string>> GetQiniuRequestHeaders(FileState state) {
IList<KeyValuePair<string, string>> GetQiniuRequestHeaders() {
IList<KeyValuePair<string, string>> makeBlockHeaders = new List<KeyValuePair<string, string>>();
string authHead = "UpToken " + state.token;
string authHead = "UpToken " + Token;
makeBlockHeaders.Add(new KeyValuePair<string, string>("Authorization", authHead));
return makeBlockHeaders;
}
async Task<Dictionary<string, object>> MakeBlock(FileState state, byte[] firstChunkBinary, long blcokSize = 4194304) {
async Task<Dictionary<string, object>> MakeBlock(byte[] firstChunkBinary, long blcokSize = 4194304) {
MemoryStream firstChunkData = new MemoryStream(firstChunkBinary, 0, firstChunkBinary.Length);
var client = new HttpClient();
@ -102,7 +125,7 @@ namespace LeanCloud.Storage.Internal {
Method = HttpMethod.Post,
Content = new StreamContent(firstChunkData)
};
var headers = GetQiniuRequestHeaders(state);
var headers = GetQiniuRequestHeaders();
foreach (var header in headers) {
request.Headers.Add(header.Key, header.Value);
}
@ -116,7 +139,7 @@ namespace LeanCloud.Storage.Internal {
return await JsonUtils.DeserializeObjectAsync<Dictionary<string, object>>(content, new LeanCloudJsonConverter());
}
async Task<Dictionary<string, object>> PutChunk(FileState state, byte[] chunkBinary, string LastChunkctx, long currentChunkOffsetInBlock) {
async Task<Dictionary<string, object>> PutChunk(byte[] chunkBinary, string LastChunkctx, long currentChunkOffsetInBlock) {
MemoryStream chunkData = new MemoryStream(chunkBinary, 0, chunkBinary.Length);
var client = new HttpClient();
var request = new HttpRequestMessage {
@ -124,7 +147,7 @@ namespace LeanCloud.Storage.Internal {
Method = HttpMethod.Post,
Content = new StreamContent(chunkData)
};
var headers = GetQiniuRequestHeaders(state);
var headers = GetQiniuRequestHeaders();
foreach (var header in headers) {
request.Headers.Add(header.Key, header.Value);
}
@ -136,13 +159,13 @@ namespace LeanCloud.Storage.Internal {
return await JsonUtils.DeserializeObjectAsync<Dictionary<string, object>>(content);
}
internal async Task<Tuple<HttpStatusCode, string>> QiniuMakeFile(FileState state, Stream dataStream, string upToken, string key, long fsize, string[] ctxes, CancellationToken cancellationToken) {
internal async Task<Tuple<HttpStatusCode, string>> QiniuMakeFile(long fsize, string[] ctxes, CancellationToken cancellationToken) {
StringBuilder urlBuilder = new StringBuilder();
urlBuilder.AppendFormat("{0}/mkfile/{1}", UP_HOST, fsize);
if (key != null) {
urlBuilder.AppendFormat("/key/{0}", ToBase64URLSafe(key));
if (!string.IsNullOrEmpty(Key)) {
urlBuilder.AppendFormat("/key/{0}", ToBase64URLSafe(Key));
}
var metaData = GetMetaData(state, dataStream);
var metaData = GetMetaData();
StringBuilder sb = new StringBuilder();
foreach (string _key in metaData.Keys) {
@ -168,7 +191,7 @@ namespace LeanCloud.Storage.Internal {
Method = HttpMethod.Post,
Content = new StreamContent(body)
};
request.Headers.Add("Authorization", $"UpToken {upToken}");
request.Headers.Add("Authorization", $"UpToken {Token}");
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("text/plain");
var response = await client.SendAsync(request);
@ -241,17 +264,17 @@ namespace LeanCloud.Storage.Internal {
}
internal IDictionary<string, object> GetMetaData(FileState state, Stream data) {
internal IDictionary<string, object> GetMetaData() {
IDictionary<string, object> rtn = new Dictionary<string, object>();
if (state.MetaData != null) {
foreach (var meta in state.MetaData) {
if (MetaData != null) {
foreach (var meta in MetaData) {
rtn.Add(meta.Key, meta.Value);
}
}
MergeDic(rtn, "mime_type", AVFile.GetMIMEType(state.Name));
MergeDic(rtn, "size", data.Length);
MergeDic(rtn, "_checksum", GetMD5Code(data));
MergeDic(rtn, "mime_type", AVFile.GetMIMEType(MimeType));
MergeDic(rtn, "size", Stream.Length);
MergeDic(rtn, "_checksum", GetMD5Code(Stream));
if (AVUser.CurrentUser != null)
if (AVUser.CurrentUser.ObjectId != null)
MergeDic(rtn, "owner", AVUser.CurrentUser.ObjectId);

View File

@ -0,0 +1,45 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace LeanCloud.Storage.Internal {
/// <summary>
/// 查询条件类
/// </summary>
public class QueryCondition : IEquatable<QueryCondition> {
public string Key {
get; set;
}
public string Op {
get; set;
}
public object Value {
get; set;
}
public bool Equals(QueryCondition other) {
return Key == other.Key && Op == other.Op;
}
public override bool Equals(object obj) {
return obj is QueryCondition && Equals(obj as QueryCondition);
}
public override int GetHashCode() {
return Key.GetHashCode() * 31 + Op.GetHashCode();
}
internal IDictionary<string, object> ToDictionary() {
return new Dictionary<string, object> {
{
Key, new Dictionary<string, object> {
{ Op, PointerOrLocalIdEncoder.Instance.Encode(Value) }
}
}
};
}
}
}

View File

@ -0,0 +1,19 @@
using System;
namespace LeanCloud.Storage.Internal {
public class QueryOperation {
public string Key {
get; set;
}
public string Op {
get; set;
}
public object Value {
get; set;
}
}
}

View File

@ -158,6 +158,7 @@ namespace LeanCloud {
AVObject.RegisterSubclass<AVUser>();
AVObject.RegisterSubclass<AVRole>();
AVObject.RegisterSubclass<AVFile>();
}
internal static void Clear() {

View File

@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
@ -10,8 +11,81 @@ namespace LeanCloud {
/// <summary>
/// AVFile 存储于 LeanCloud 的文件类
/// </summary>
public class AVFile : IJsonConvertible {
private FileState state;
[AVClassName("_File")]
public class AVFile : AVObject {
const string QCloud = "qcloud";
const string AWS = "s3";
#region Properties
/// <summary>
/// Gets the name of the file. Before save is called, this is the filename given by
/// the user. After save is called, that name gets prefixed with a unique identifier.
/// </summary>
[AVFieldName("name")]
public string Name {
get {
return GetProperty<string>("Name");
}
set {
SetProperty(value, "Name");
}
}
[AVFieldName("key")]
public string Key {
get {
return GetProperty<string>("Key");
}
set {
SetProperty(value, "Key");
}
}
/// <summary>
/// Gets the MIME type of the file. This is either passed in to the constructor or
/// inferred from the file extension. "unknown/unknown" will be used if neither is
/// available.
/// </summary>
[AVFieldName("mime_type")]
public string MimeType {
get {
return GetProperty<string>("MimeType");
}
set {
SetProperty(value, "MimeType");
}
}
/// <summary>
/// Gets the url of the file. It is only available after you save the file or after
/// you get the file from a <see cref="AVObject"/>.
/// </summary>
[AVFieldName("url")]
public string Url {
get {
return GetProperty<string>("Url");
}
set {
SetProperty(value, "Url");
}
}
/// <summary>
/// 文件的元数据。
/// </summary>
[AVFieldName("metaData")]
public IDictionary<string, object> MetaData {
get {
return GetProperty<IDictionary<string, object>>("MetaData");
}
set {
SetProperty(value, "MetaData");
}
}
#endregion
private readonly Stream dataStream;
#region Constructor
@ -23,12 +97,9 @@ namespace LeanCloud {
/// <param name="mimeType">文件类型</param>
/// <param name="metaData">文件源信息</param>
public AVFile(string name, Stream data, string mimeType = null, IDictionary<string, object> metaData = null) {
mimeType = mimeType ?? GetMIMEType(name);
state = new FileState {
Name = name,
MimeType = mimeType,
MetaData = metaData
};
Name = name;
MimeType = mimeType ?? GetMIMEType(name);
MetaData = metaData;
dataStream = data;
}
@ -92,6 +163,10 @@ namespace LeanCloud {
}
public AVFile() {
}
#endregion
#region created by url or uri
@ -104,13 +179,8 @@ namespace LeanCloud {
/// <param name="mimeType">文件类型</param>
/// <param name="metaData">文件源信息</param>
public AVFile(string name, Uri uri, string mimeType = null, IDictionary<string, object> metaData = null) {
mimeType = mimeType == null ? GetMIMEType(name) : mimeType;
state = new FileState {
Name = name,
Url = uri,
MetaData = metaData,
MimeType = mimeType
};
Name = name;
MimeType = mimeType ?? GetMIMEType(name);
IsExternal = true;
}
@ -161,66 +231,15 @@ namespace LeanCloud {
/// </summary>
/// <param name="name">文件名</param>
/// <param name="url">文件的 Url</param>
public AVFile(string name, string url)
: this(name, new Uri(url)) {
}
internal AVFile(FileState filestate) {
state = filestate;
}
internal AVFile(string objectId)
: this(new FileState {
ObjectId = objectId
}) {
public AVFile(string name, string url) {
Name = name;
Url = url;
IsExternal = true;
}
#endregion
#region Properties
/// <summary>
/// Gets whether the file still needs to be saved.
/// </summary>
public bool IsDirty {
get {
return state.Url == null;
}
}
/// <summary>
/// Gets the name of the file. Before save is called, this is the filename given by
/// the user. After save is called, that name gets prefixed with a unique identifier.
/// </summary>
[AVFieldName("name")]
public string Name {
get {
return state.Name;
}
}
/// <summary>
/// Gets the MIME type of the file. This is either passed in to the constructor or
/// inferred from the file extension. "unknown/unknown" will be used if neither is
/// available.
/// </summary>
public string MimeType {
get {
return state.MimeType;
}
}
/// <summary>
/// Gets the url of the file. It is only available after you save the file or after
/// you get the file from a <see cref="AVObject"/>.
/// </summary>
[AVFieldName("url")]
public Uri Url {
get {
return state.Url;
}
}
/// <summary>
/// 获取缩略图
@ -243,9 +262,7 @@ namespace LeanCloud {
}
}
#endregion
IDictionary<string, object> IJsonConvertible.ToJSON() {
IDictionary<string, object> ToJSON() {
if (IsDirty) {
throw new InvalidOperationException(
"AVFile must be saved before it can be serialized.");
@ -254,7 +271,7 @@ namespace LeanCloud {
{ "__type", "File"} ,
{ "id", ObjectId },
{ "name", Name },
{ "url", Url.AbsoluteUri }
{ "url", Url }
};
}
@ -263,34 +280,57 @@ namespace LeanCloud {
/// <summary>
/// 保存文件
/// </summary>
/// <param name="progress">The progress callback.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public async Task SaveAsync(IProgress<AVUploadProgressEventArgs> progress = null, CancellationToken cancellationToken = default) {
state = await FileController.SaveAsync(state, dataStream, progress, cancellationToken);
public override async Task SaveAsync(bool fetchWhenSave = false, AVQuery<AVObject> query = null, CancellationToken cancellationToken = default) {
if (IsExternal) {
await base.SaveAsync(fetchWhenSave, query, cancellationToken);
} else {
// 需不需要判断数据的 dirty?
// 获取 FileToken 并创建对应记录
Tuple<HttpStatusCode, IDictionary<string, object>> res = await AVPlugins.Instance.FileController.GetFileToken(Name, MetaData);
IObjectState serverState = AVObjectCoder.Instance.Decode(res.Item2, AVDecoder.Instance);
HandleSave(serverState);
IDictionary<string, object> fileData = res.Item2;
AVClient.PrintLog(fileData.ToString());
var provider = fileData["provider"] as string;
switch (provider) {
case QCloud: {
await new QCloudUploader {
FileName = Key,
UploadUrl = fileData["upload_url"] as string,
Token = fileData["token"] as string,
Bucket = fileData["bucket"] as string,
Stream = dataStream
}.Upload();
}
break;
case AWS: {
await new AWSUploader {
UploadUrl = fileData["upload_url"] as string,
MimeType = MimeType,
Stream = dataStream
}.Upload();
}
break;
default: {
await new QiniuUploader {
Key = Key,
Token = fileData["token"] as string,
MimeType = MimeType,
MetaData = MetaData,
Stream = dataStream
}.Upload();
}
break;
}
}
}
#endregion
#region Compatible
/// <summary>
/// 文件在 LeanCloud 的唯一Id 标识
/// </summary>
public string ObjectId {
get {
return state.ObjectId;
}
}
/// <summary>
/// 文件的元数据。
/// </summary>
public IDictionary<string, object> MetaData {
get {
return state.MetaData;
}
}
/// <summary>
/// 文件是否为外链文件。
/// </summary>
@ -300,6 +340,13 @@ namespace LeanCloud {
get; private set;
}
internal static string GetUniqueName(string name) {
string key = Guid.NewGuid().ToString();
string extension = Path.GetExtension(name);
key += extension;
return key;
}
internal static string GetMIMEType(string fileName) {
try {
string str = Path.GetExtension(fileName).Remove(0, 1);
@ -312,48 +359,16 @@ namespace LeanCloud {
}
}
/// <summary>
/// 根据 ObjectId 获取文件
/// </summary>
/// <remarks>获取之后并没有实际执行下载只是加载了文件的元信息以及物理地址Url
/// </remarks>
public static async Task<AVFile> GetFileWithObjectIdAsync(string objectId, CancellationToken cancellationToken = default) {
var fileState = await FileController.GetAsync(objectId, cancellationToken);
return new AVFile(fileState);
}
public static AVFile CreateWithoutData(string objectId) {
return new AVFile(objectId);
}
public static AVFile CreateWithState(FileState state) {
return new AVFile(state);
}
public static AVFile CreateWithData(string objectId, string name, string url, IDictionary<string, object> metaData) {
var fileState = new FileState {
Name = name,
ObjectId = objectId,
Url = new Uri(url),
MetaData = metaData
};
return CreateWithState(fileState);
}
//public static AVFile CreateWithData(string objectId, string name, string url, IDictionary<string, object> metaData) {
// var fileState = new FileState {
// Name = name,
// ObjectId = objectId,
// Url = new Uri(url),
// MetaData = metaData
// };
// return CreateWithState(fileState);
//}
internal void MergeFromJSON(IDictionary<string, object> data) {
state = FileState.NewFromDict(data);
}
/// <summary>
/// 删除文件
/// </summary>
/// <returns>Task</returns>
public Task DeleteAsync(CancellationToken cancellationToken = default) {
if (ObjectId == null) {
return Task.FromException(new ArgumentNullException(nameof(ObjectId)));
}
return FileController.DeleteAsync(state, cancellationToken);
}
#endregion
private readonly static Dictionary<string, string> MIMETypesDictionary = new Dictionary<string, string> {

View File

@ -806,7 +806,7 @@ string propertyName
/// Deletes this object on the server.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
public async Task DeleteAsync(CancellationToken cancellationToken = default) {
public virtual async Task DeleteAsync(CancellationToken cancellationToken = default) {
if (ObjectId == null) {
return;
}

View File

@ -60,7 +60,7 @@ namespace LeanCloud {
ClassName = className;
}
private AVQuery(AVQuery<T> source,
protected AVQuery(AVQuery<T> source,
IDictionary<string, object> where = null,
IEnumerable<string> replacementOrderBy = null,
IEnumerable<string> thenBy = null,
@ -202,18 +202,18 @@ namespace LeanCloud {
});
}
public async Task<IEnumerable<T>> FindAsync(CancellationToken cancellationToken = default) {
public virtual async Task<IEnumerable<T>> FindAsync(CancellationToken cancellationToken = default) {
IEnumerable<IObjectState> states = await QueryController.FindAsync(this, AVUser.CurrentUser, cancellationToken);
return (from state in states
select AVObject.FromState<T>(state, ClassName));
}
public async Task<T> FirstOrDefaultAsync(CancellationToken cancellationToken = default) {
public virtual async Task<T> FirstOrDefaultAsync(CancellationToken cancellationToken = default) {
IObjectState state = await QueryController.FirstAsync<T>(this, AVUser.CurrentUser, cancellationToken);
return state == null ? default : AVObject.FromState<T>(state, ClassName);
}
public async Task<T> FirstAsync(CancellationToken cancellationToken = default) {
public virtual async Task<T> FirstAsync(CancellationToken cancellationToken = default) {
var result = await FirstOrDefaultAsync(cancellationToken);
if (result == null) {
throw new AVException(AVException.ErrorCode.ObjectNotFound,
@ -222,11 +222,11 @@ namespace LeanCloud {
return result;
}
public Task<int> CountAsync(CancellationToken cancellationToken = default) {
public virtual Task<int> CountAsync(CancellationToken cancellationToken = default) {
return QueryController.CountAsync(this, AVUser.CurrentUser, cancellationToken);
}
public async Task<T> GetAsync(string objectId, CancellationToken cancellationToken) {
public virtual async Task<T> GetAsync(string objectId, CancellationToken cancellationToken) {
AVQuery<T> singleItemQuery = new AVQuery<T>(ClassName)
.WhereEqualTo("objectId", objectId);
singleItemQuery = new AVQuery<T>(singleItemQuery, includes: this.includes, selectedKeys: this.selectedKeys, limit: 1);

View File

@ -33,6 +33,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppRouter", "AppRouter\AppR
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Storage", "Storage\Storage\Storage.csproj", "{59DA32A0-4CD3-424A-8584-D08B8D1E2B98}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RTM", "RTM\RTM\RTM.csproj", "{D4A30F70-AAED-415D-B940-023B3D7241EE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -83,6 +85,10 @@ Global
{59DA32A0-4CD3-424A-8584-D08B8D1E2B98}.Debug|Any CPU.Build.0 = Debug|Any CPU
{59DA32A0-4CD3-424A-8584-D08B8D1E2B98}.Release|Any CPU.ActiveCfg = Release|Any CPU
{59DA32A0-4CD3-424A-8584-D08B8D1E2B98}.Release|Any CPU.Build.0 = Release|Any CPU
{D4A30F70-AAED-415D-B940-023B3D7241EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D4A30F70-AAED-415D-B940-023B3D7241EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D4A30F70-AAED-415D-B940-023B3D7241EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D4A30F70-AAED-415D-B940-023B3D7241EE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{659D19F0-9A40-42C0-886C-555E64F16848} = {CD6B6669-1A56-437A-932E-BCE7F5D4CD18}
@ -96,6 +102,7 @@ Global
{BE05B492-78CD-47CA-9F48-C3E9B4813AFF} = {CD6B6669-1A56-437A-932E-BCE7F5D4CD18}
{D34FC092-042A-44CE-A9E2-56B996BDCF42} = {1F05195D-2CAA-4214-8DAE-FE14A6B905DD}
{59DA32A0-4CD3-424A-8584-D08B8D1E2B98} = {CD6B6669-1A56-437A-932E-BCE7F5D4CD18}
{D4A30F70-AAED-415D-B940-023B3D7241EE} = {64D8F9A1-BA44-459C-817C-788B4EBC0B9F}
EndGlobalSection
GlobalSection(MonoDevelopProperties) = preSolution
version = 0.1.0